switchroom 0.18.18 → 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 (45) 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 +1131 -285
  7. package/telegram-plugin/dist/server.js +6 -0
  8. package/telegram-plugin/format.ts +208 -125
  9. package/telegram-plugin/gateway/cron-session.ts +32 -0
  10. package/telegram-plugin/gateway/gateway.ts +800 -107
  11. package/telegram-plugin/gateway/idle-clear.ts +170 -0
  12. package/telegram-plugin/gateway/inject-handler.ts +11 -0
  13. package/telegram-plugin/gateway/outbound-send-path.ts +5 -3
  14. package/telegram-plugin/gateway/turn-record-status.ts +134 -0
  15. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +23 -0
  16. package/telegram-plugin/hooks/silent-end-scan.mjs +98 -8
  17. package/telegram-plugin/llm-error-present.ts +68 -30
  18. package/telegram-plugin/narrative-flush.ts +181 -0
  19. package/telegram-plugin/pending-work-progress.ts +65 -1
  20. package/telegram-plugin/session-tail.ts +6 -1
  21. package/telegram-plugin/silent-end.ts +182 -0
  22. package/telegram-plugin/subagent-watcher.ts +244 -81
  23. package/telegram-plugin/tests/answer-ready-flush.test.ts +343 -0
  24. package/telegram-plugin/tests/cron-inject-idle-clock.test.ts +54 -0
  25. package/telegram-plugin/tests/emission-authority-facade.test.ts +13 -10
  26. package/telegram-plugin/tests/format-consistency.test.ts +39 -4
  27. package/telegram-plugin/tests/gateway-outbound-redact.test.ts +26 -0
  28. package/telegram-plugin/tests/idle-clear.test.ts +315 -37
  29. package/telegram-plugin/tests/llm-error-present.test.ts +110 -9
  30. package/telegram-plugin/tests/narrative-flush.test.ts +213 -0
  31. package/telegram-plugin/tests/narrative-splice-before-finalize.test.ts +167 -0
  32. package/telegram-plugin/tests/outbound-send-path.test.ts +2 -0
  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/subagent-watcher-narrative-early-paint.test.ts +218 -0
  38. package/telegram-plugin/tests/telegram-format.test.ts +72 -4
  39. package/telegram-plugin/tests/turn-record-status.test.ts +119 -0
  40. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +218 -1
  41. package/telegram-plugin/tests/worker-feed-terminal-cleanup.test.ts +254 -0
  42. package/telegram-plugin/tests/worker-feed-terminal-state-truthful.test.ts +125 -0
  43. package/telegram-plugin/tool-activity-summary.ts +78 -16
  44. package/telegram-plugin/turn-flush-safety.ts +2 -1
  45. package/telegram-plugin/worker-activity-feed.ts +181 -30
@@ -6835,12 +6835,13 @@ function addParagraphSpacers(text) {
6835
6835
 
6836
6836
  `))
6837
6837
  return restore(masked);
6838
- const spacerLine = PARAGRAPH_SPACER;
6839
- const isBlankLine = (line) => /^[ \t\r\f\v]*$/.test(line);
6838
+ const SP = PARAGRAPH_SPACER;
6840
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
+ };
6841
6844
  const blockKind = (line) => {
6842
- if (asciiTrim(line) === spacerLine)
6843
- return "spacer";
6844
6845
  if (isFenceOpenLine(line, placeholder))
6845
6846
  return "fence";
6846
6847
  if (isListItemLine(line))
@@ -6865,58 +6866,65 @@ function addParagraphSpacers(text) {
6865
6866
  const shouldSpaceGap = (above, below) => {
6866
6867
  const a = blockKind(above);
6867
6868
  const b = blockKind(below);
6868
- if (a === "spacer" || b === "spacer")
6869
- return false;
6870
6869
  if (a === b && SAME_KIND_TIGHT.has(a))
6871
6870
  return false;
6872
6871
  return true;
6873
6872
  };
6874
- const lines = masked.split(`
6875
- `);
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
+ }
6876
6883
  const out = [];
6877
- for (let i = 0;i < lines.length; i++) {
6878
- const line = lines[i];
6879
- const isBlank = isBlankLine(line);
6880
- if (isBlank) {
6881
- const prevEmitted = out.length > 0 ? out[out.length - 1] : null;
6882
- const prevIsBlank = prevEmitted != null && isBlankLine(prevEmitted);
6883
- if (!prevIsBlank) {
6884
- const above = lastNonBlank(out, isBlankLine);
6885
- const below = nextNonBlank(lines, i + 1, isBlankLine);
6886
- const alreadySpaced = above != null && asciiTrim(above) === spacerLine || below != null && asciiTrim(below) === spacerLine;
6887
- if (!alreadySpaced && above != null && below != null && shouldSpaceGap(above, below)) {
6888
- out.push("");
6889
- out.push(spacerLine);
6890
- out.push("");
6891
- continue;
6892
- }
6893
- }
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);
6894
6901
  }
6895
- out.push(line);
6896
6902
  }
6897
6903
  return restore(out.join(`
6898
6904
  `));
6899
6905
  }
6900
- function lastNonBlank(arr, isBlank) {
6901
- for (let i = arr.length - 1;i >= 0; i--) {
6902
- if (!isBlank(arr[i]))
6903
- return arr[i];
6904
- }
6905
- return null;
6906
- }
6907
- function nextNonBlank(lines, from, isBlank) {
6908
- for (let i = from;i < lines.length; i++) {
6909
- if (!isBlank(lines[i]))
6910
- return lines[i];
6911
- }
6912
- return null;
6913
- }
6914
6906
  function normalizePunctuation(text) {
6915
6907
  if (!/[\u2014\u2013\u2022\u00b7]/.test(text))
6916
6908
  return text;
6917
6909
  const nonce = Math.random().toString(36).slice(2);
6918
6910
  const { masked, restore } = maskCodeRegions(text, nonce);
6919
- let out = masked.replace(/(\S)[ \t][\u2014\u2013][ \t](?=(\S))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2014(?=(\w))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2013(?=\w)/g, "$1-");
6911
+ const linkMasks = [];
6912
+ const LINK_MASK_PH = `\x00RML${nonce}_`;
6913
+ const maskedLinks = masked.replace(/(\]\()([^)\n]*)(\))/g, (_m, open, href, close) => {
6914
+ const idx = linkMasks.length;
6915
+ linkMasks.push(href);
6916
+ return `${open}${LINK_MASK_PH}${idx}\x00${close}`;
6917
+ });
6918
+ const maskedAutolinks = maskedLinks.replace(/(<)([a-zA-Z][a-zA-Z0-9+.-]*:[^>\s]*)(>)/g, (_m, lt, uri, gt) => {
6919
+ const idx = linkMasks.length;
6920
+ linkMasks.push(uri);
6921
+ return `${lt}${LINK_MASK_PH}${idx}\x00${gt}`;
6922
+ });
6923
+ const escNonce = nonce.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6924
+ const linkRestoreRe = new RegExp(`\x00RML${escNonce}_(\\d+)\x00`, "g");
6925
+ const restoreLinks = (s) => s.replace(linkRestoreRe, (_m, idx) => linkMasks[Number(idx)] ?? _m);
6926
+ let out = maskedAutolinks.replace(/(\S)[ \t][\u2014\u2013][ \t](?=(\S))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2014(?=(\w))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2013(?=\w)/g, "$1-");
6927
+ out = restoreLinks(out);
6920
6928
  out = out.split(`
6921
6929
  `).map((line) => line.replace(/^([ \t]*)[\u2022\u00b7][ \t]+/, "$1- ")).join(`
6922
6930
  `);
@@ -7109,6 +7117,7 @@ function splitMarkdownChunks(text, maxLen = RICH_MESSAGE_MAX_CHARS) {
7109
7117
  }
7110
7118
  cut = backOffOpenFence(rest, cut);
7111
7119
  cut = backOffTableRow(rest, cut);
7120
+ cut = backOffOpenInline(rest, cut);
7112
7121
  if (cut <= 0) {
7113
7122
  const sliced = hardSliceToCap(rest, maxLen);
7114
7123
  chunks.push(stripBoundarySpacers(sliced[0], "trailing"));
@@ -7154,7 +7163,38 @@ function backOffTableRow(text, cut) {
7154
7163
  }
7155
7164
  return cut;
7156
7165
  }
7157
- var RICH_MESSAGE_MAX_CHARS = 32768, PARAGRAPH_SPACER = "\u00a0";
7166
+ function backOffOpenInline(text, cut) {
7167
+ if (cut <= 0 || cut >= text.length)
7168
+ return cut;
7169
+ let earliest = cut;
7170
+ for (const re of INLINE_SPAN_PATTERNS) {
7171
+ re.lastIndex = 0;
7172
+ let m;
7173
+ while ((m = re.exec(text)) !== null) {
7174
+ const start = m.index;
7175
+ const end = start + m[0].length;
7176
+ if (start < cut && cut < end && start < earliest)
7177
+ earliest = start;
7178
+ if (start >= cut)
7179
+ break;
7180
+ if (re.lastIndex === start)
7181
+ re.lastIndex = start + 1;
7182
+ }
7183
+ }
7184
+ return earliest;
7185
+ }
7186
+ var RICH_MESSAGE_MAX_CHARS = 32768, PARAGRAPH_SPACER = "\u00a0", INLINE_SPAN_PATTERNS;
7187
+ var init_format = __esm(() => {
7188
+ INLINE_SPAN_PATTERNS = [
7189
+ /`[^`\n]+`/g,
7190
+ /\*\*\*[^*\n]+\*\*\*/g,
7191
+ /___[^_\n]+___/g,
7192
+ /\*\*[^*\n]+\*\*/g,
7193
+ /__[^_\n]+__/g,
7194
+ /(?<![\w*])_[^_\n]+_(?![\w*])/g,
7195
+ /\[[^\]\n]*\]\([^)\n]*\)/g
7196
+ ];
7197
+ });
7158
7198
 
7159
7199
  // text-voice-scrub.ts
7160
7200
  function enabled() {
@@ -7339,6 +7379,7 @@ function cleanWorkerResultParagraph(s) {
7339
7379
  return kept.join(" ").replace(/\s+/g, " ").trim();
7340
7380
  }
7341
7381
  var init_card_format = __esm(() => {
7382
+ init_format();
7342
7383
  init_text_voice_scrub();
7343
7384
  });
7344
7385
 
@@ -7480,6 +7521,7 @@ function ttlMsFromToken(token) {
7480
7521
  }
7481
7522
  var import_grammy3;
7482
7523
  var init_approval_card = __esm(() => {
7524
+ init_format();
7483
7525
  import_grammy3 = __toESM(require_mod2(), 1);
7484
7526
  });
7485
7527
 
@@ -36885,6 +36927,7 @@ function parseConfigApprovalCallback(data) {
36885
36927
  var pending, TELEGRAM_SENDMESSAGE_LIMIT2 = 32768, RENDERED_BODY_CAP2 = 32000, REASON_MAX_CHARS = 500, REASON_ELLIPSIS = "\u2026", DIFF_SENTINEL = `
36886
36928
  [\u2026 diff continues, see attached file]`;
36887
36929
  var init_config_approval_handler = __esm(() => {
36930
+ init_format();
36888
36931
  pending = new Map;
36889
36932
  });
36890
36933
 
@@ -37009,6 +37052,7 @@ ${detail}`)));
37009
37052
  });
37010
37053
  }
37011
37054
  var init_approvals_commands = __esm(() => {
37055
+ init_format();
37012
37056
  init_rich_send();
37013
37057
  init_client3();
37014
37058
  });
@@ -39778,6 +39822,7 @@ class DeferredDoneReactions {
39778
39822
  init_card_format();
39779
39823
 
39780
39824
  // status-no-truncate.ts
39825
+ init_format();
39781
39826
  var STATUS_ROLLING_LINES = 5;
39782
39827
  var STATUS_LINE_MAX = 200;
39783
39828
  var STATUS_CARD_CHAR_BUDGET = RICH_MESSAGE_MAX_CHARS;
@@ -40252,27 +40297,43 @@ function renderActivityFeedWithNested(lines, childLines, final = false, liveSuff
40252
40297
  });
40253
40298
  }
40254
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
+ }
40255
40308
  function renderCombinedWorkerFeed(rows, opts) {
40256
40309
  if (rows.length === 0)
40257
40310
  return null;
40258
40311
  const maxRows = Math.max(1, Math.floor(opts.maxRows));
40259
- const rowLines = (r) => {
40312
+ const rowHeader = (r) => {
40260
40313
  const desc = escapeMarkdown(truncate(stripMarkdown(r.description).replace(/\s+/g, " ").trim() || "background task", COMBINED_ROW_DESC_MAX));
40261
40314
  const toolWord = r.toolCount === 1 ? "tool" : "tools";
40262
40315
  const modelLabel = formatModelLabel(r.model);
40263
40316
  const modelPart = modelLabel != null ? ` \u00b7 ${escapeMarkdown(modelLabel)}` : "";
40264
- const header = `**${desc}** _\u00b7 ${formatFeedElapsed(r.elapsedMs)} \u00b7 ${r.toolCount} ${toolWord}${modelPart}_`;
40265
- const stepClean = stripMarkdown(r.currentStep).replace(/\s+/g, " ").trim();
40266
- const step = stepClean.length > 0 ? `\u2192 _${escapeMarkdown(truncate(stepClean, STATUS_LINE_MAX))}_` : `\u2192 _starting\u2026_`;
40267
- 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);
40268
40322
  };
40269
40323
  const compose = (visibleCount) => {
40270
40324
  const shown = rows.slice(0, visibleCount);
40271
40325
  const hidden = rows.length - shown.length;
40326
+ const depth = combinedHistoryDepth(shown.length);
40272
40327
  const out = [`\uD83D\uDEE0 **Workers** \u00b7 _${rows.length} running_`];
40273
40328
  for (const r of shown) {
40274
- const [h, s] = rowLines(r);
40275
- 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);
40276
40337
  }
40277
40338
  if (hidden > 0)
40278
40339
  out.push(`_+${hidden} more working\u2026_`);
@@ -40349,7 +40410,7 @@ function isWorkerActivityFeedEnabled(envVal) {
40349
40410
  var DESC_MAX = 80;
40350
40411
  function renderWorkerActivity(v, liveSuffix = "") {
40351
40412
  const desc = truncate(stripMarkdown(v.description).trim() || "background task", DESC_MAX);
40352
- const finished = v.state === "done" || v.state === "failed";
40413
+ const finished = v.state === "done" || v.state === "failed" || v.state === "incomplete";
40353
40414
  const rawSteps = (v.narrativeLines ?? []).filter((s) => s != null && s.trim().length > 0);
40354
40415
  let steps = rawSteps;
40355
40416
  if (steps.length === 0 && !finished) {
@@ -40367,7 +40428,7 @@ function renderWorkerActivity(v, liveSuffix = "") {
40367
40428
  model: v.model
40368
40429
  };
40369
40430
  let result;
40370
- if (finished) {
40431
+ if (finished && v.state !== "incomplete") {
40371
40432
  const text = cleanWorkerResultParagraph(v.latestSummary);
40372
40433
  if (text.length > 0)
40373
40434
  result = { emoji: v.state === "done" ? "\u2705" : "\u26a0\ufe0f", text };
@@ -40421,6 +40482,7 @@ function createWorkerActivityFeed(opts) {
40421
40482
  const firstPaintMin = opts.firstPaintMinMs ?? 8000;
40422
40483
  const heartbeatTickMs = opts.heartbeatTickMs ?? 6000;
40423
40484
  const maxRows = Math.max(1, Math.floor(opts.maxRows ?? 8));
40485
+ const staleWorkerTtlMs = Math.max(1, Math.floor(opts.staleWorkerTtlMs ?? 50 * 60000));
40424
40486
  const reconcilePinFn = opts.reconcilePin ?? (() => {});
40425
40487
  const setIntervalFn = opts.setInterval ?? ((cb, ms) => {
40426
40488
  const t = setInterval(cb, ms);
@@ -40522,6 +40584,7 @@ function createWorkerActivityFeed(opts) {
40522
40584
  elapsedMs: elapsedFor(r),
40523
40585
  toolCount: v.toolCount,
40524
40586
  currentStep,
40587
+ historyLines: r.narrative.length > 0 ? [...r.narrative] : undefined,
40525
40588
  model: v.model
40526
40589
  };
40527
40590
  });
@@ -40641,8 +40704,60 @@ function createWorkerActivityFeed(opts) {
40641
40704
  log(`worker-feed: edit transient error feed=${g.feedKey}: ${err.message}`);
40642
40705
  }
40643
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
+ }
40644
40747
  function heartbeatTick() {
40645
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
+ }
40646
40761
  for (const g of [...groups.values()]) {
40647
40762
  if (g.pendingFinalize != null && now >= g.cooldownUntil) {
40648
40763
  const recap = g.pendingFinalize;
@@ -40728,12 +40843,14 @@ function createWorkerActivityFeed(opts) {
40728
40843
  lastView: null,
40729
40844
  state: "running",
40730
40845
  finished: false,
40846
+ lastUpdateAt: nowFn(),
40731
40847
  dispatchAtMs: null,
40732
40848
  stepStartedAtMs: null
40733
40849
  };
40734
40850
  g.workers.set(agentId, row);
40735
40851
  agentIndex.set(agentId, feedKey);
40736
40852
  }
40853
+ row.lastUpdateAt = nowFn();
40737
40854
  accumulateNarrative(row, view);
40738
40855
  row.state = "running";
40739
40856
  row.lastView = { ...view, narrativeLines: [...row.narrative] };
@@ -40752,23 +40869,10 @@ function createWorkerActivityFeed(opts) {
40752
40869
  markFinalized(agentId);
40753
40870
  return Promise.resolve();
40754
40871
  }
40755
- row.finished = true;
40756
- row.state = view.state === "failed" ? "failed" : "done";
40757
- markFinalized(agentId);
40758
- const group = g;
40759
- group.chain = group.chain.then(() => {
40760
- const others = runningRows(group).filter((w) => w.agentId !== agentId);
40761
- if (others.length > 0) {
40762
- removeWorker(group, agentId);
40763
- syncPin(group);
40764
- return doRender(group, { force: true });
40765
- }
40766
- const recap = { ...view, narrativeLines: [...row.narrative] };
40767
- return doRender(group, { force: true, terminalRecap: recap, finishingAgentId: agentId });
40768
- }).catch((err) => {
40769
- log(`worker-feed: finish chain error ${agentId}: ${err.message}`);
40770
- });
40771
- return group.chain;
40872
+ return finalizeWorker(g, agentId, row, view);
40873
+ },
40874
+ terminate(agentId) {
40875
+ return terminateWorker(agentId);
40772
40876
  },
40773
40877
  drop(agentId) {
40774
40878
  markFinalized(agentId);
@@ -41247,6 +41351,7 @@ var import_grammy2 = __toESM(require_mod2(), 1);
41247
41351
  var import_runner = __toESM(require_mod4(), 1);
41248
41352
  import { AsyncLocalStorage } from "async_hooks";
41249
41353
  init_flood_circuit_breaker();
41354
+ init_format();
41250
41355
 
41251
41356
  // shared/gw-trace-gate.ts
41252
41357
  function computeGwTraceVerbose(flag) {
@@ -41281,6 +41386,7 @@ function escapeHtmlForTg(text) {
41281
41386
 
41282
41387
  // gateway/vault-request-access-card.ts
41283
41388
  init_approval_card();
41389
+ init_format();
41284
41390
  function renderVaultRequestAccessCard(req) {
41285
41391
  const lines = [];
41286
41392
  const scopeLabel = req.scope === "write" ? "write" : "read";
@@ -45070,6 +45176,9 @@ function runSilentTurnHeartbeatTick(view, deps) {
45070
45176
 
45071
45177
  // narrative-dedup.ts
45072
45178
  var REPLY_TOOLS = new Set(["reply", "stream_reply"]);
45179
+
45180
+ // narrative-dedup.ts
45181
+ var REPLY_TOOLS2 = new Set(["reply", "stream_reply"]);
45073
45182
  function normalizeNarrative(s) {
45074
45183
  return s.replace(/[*_`>#~]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
45075
45184
  }
@@ -45089,6 +45198,83 @@ function isDraftOfReply(textBlock, replyText) {
45089
45198
  return prefixSimilarity(textBlock, replyText) >= DRAFT_SUPPRESS_THRESHOLD;
45090
45199
  }
45091
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
+
45092
45278
  // tool-labels.ts
45093
45279
  var MAX_LABEL_CHARS = 60;
45094
45280
  var MAX_BASH_CHARS = 40;
@@ -54817,6 +55003,7 @@ function parse2(markdown) {
54817
55003
  }
54818
55004
 
54819
55005
  // render/render.ts
55006
+ init_format();
54820
55007
  function renderInline(node2, ctx = {}) {
54821
55008
  switch (node2.type) {
54822
55009
  case "plain":
@@ -55022,6 +55209,7 @@ function renderSafe(doc, source, maxLen = RICH_MESSAGE_MAX_CHARS) {
55022
55209
  }
55023
55210
 
55024
55211
  // render/rich-render.ts
55212
+ init_format();
55025
55213
  var PLAIN_TEXT_MAX_CHARS = 4096;
55026
55214
  function parseRichRenderEnabled(raw) {
55027
55215
  if (raw == null)
@@ -55307,6 +55495,10 @@ function handlePtyPartialPure(text4, state, deps) {
55307
55495
  stream.update(text4).catch(() => {});
55308
55496
  return created ? "update-new" : "update-existing";
55309
55497
  }
55498
+
55499
+ // stream-reply-handler.ts
55500
+ init_format();
55501
+
55310
55502
  // chat-lock.ts
55311
55503
  function createChatLock() {
55312
55504
  const chains = new Map;
@@ -56401,6 +56593,7 @@ var import_runner2 = __toESM(require_mod4(), 1);
56401
56593
  import { createHash as createHash2 } from "crypto";
56402
56594
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
56403
56595
  init_flood_circuit_breaker();
56596
+ init_format();
56404
56597
  var tgPostTagStore2 = new AsyncLocalStorage2;
56405
56598
  function _getTgPostTags() {
56406
56599
  return tgPostTagStore2.getStore();
@@ -61497,6 +61690,7 @@ function decideOverPing(input) {
61497
61690
  }
61498
61691
 
61499
61692
  // silent-reply-anchor.ts
61693
+ init_format();
61500
61694
  var TELEGRAM_MSG_CAP = RICH_MESSAGE_MAX_CHARS;
61501
61695
  function enabled3() {
61502
61696
  const v = process.env.SWITCHROOM_DISABLE_SILENT_REPLY_AUTOEDIT;
@@ -61813,6 +62007,7 @@ function startTimer(deps) {
61813
62007
  var EDIT_INTERVAL_MS = 60000;
61814
62008
  var POLL_INTERVAL_MS = 5000;
61815
62009
  var MAX_LIFETIME_MS = 30 * 60000;
62010
+ var BACKGROUND_WORK_SUPPRESS_TTL_MS = MAX_LIFETIME_MS;
61816
62011
  var TELEGRAM_MSG_CAP2 = 32768;
61817
62012
  var SUFFIX_RE = /\n\n(?:\u2014 |_)still working \(\d+m\)( \u00b7 message me anytime, I'll keep you posted)?_?$/;
61818
62013
  var stateByKey = new Map;
@@ -61830,6 +62025,7 @@ function ensure(key) {
61830
62025
  if (!s) {
61831
62026
  s = {
61832
62027
  pending: false,
62028
+ dispatchedAt: null,
61833
62029
  anchorMessageId: null,
61834
62030
  anchorOriginalText: "",
61835
62031
  anchorLiteralText: false,
@@ -61843,7 +62039,9 @@ function ensure(key) {
61843
62039
  function noteAsyncDispatch(key) {
61844
62040
  if (!enabled4())
61845
62041
  return;
61846
- ensure(key).pending = true;
62042
+ const s = ensure(key);
62043
+ s.pending = true;
62044
+ s.dispatchedAt = nowMs();
61847
62045
  }
61848
62046
  function noteOutbound3(key, opts) {
61849
62047
  if (!enabled4())
@@ -61873,6 +62071,17 @@ function noteTurnEnd(key) {
61873
62071
  function hasPendingAsyncDispatch(key) {
61874
62072
  return stateByKey.get(key)?.pending === true;
61875
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
+ }
61876
62085
  function clearPending(key, reason) {
61877
62086
  if (!stateByKey.has(key))
61878
62087
  return;
@@ -62040,6 +62249,30 @@ function clearSilentEndState(turnKey, deps) {
62040
62249
  `);
62041
62250
  } catch {}
62042
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
+ }
62043
62276
  function readSilentEndState(deps) {
62044
62277
  const statePath = resolveStatePath2(deps);
62045
62278
  if (!existsSync14(statePath))
@@ -62145,6 +62378,56 @@ function isSilentFlushMarker(text4) {
62145
62378
  return SILENT_MARKERS.has(trimmed.toUpperCase());
62146
62379
  }
62147
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
+ }
62148
62431
 
62149
62432
  // answer-stream.ts
62150
62433
  var MIN_INITIAL_CHARS = 50;
@@ -62634,6 +62917,7 @@ function safeResolvePersonName(directory, opts, rawFallback) {
62634
62917
  }
62635
62918
 
62636
62919
  // gateway/auth-command.ts
62920
+ init_format();
62637
62921
  init_auth_snapshot_format();
62638
62922
  init_demo_mask();
62639
62923
  var AUTH_RM_CONFIRM_TTL_MS = 60000;
@@ -64783,6 +65067,10 @@ function autoClassifyMidTurnInbound(i) {
64783
65067
  return { decision: "queue", reason: "cross_topic", sameTopic: false };
64784
65068
  return recent && i.msSinceLastAgentOutput <= i.topicSteerWindowMs ? { decision: "steer", reason: "same_topic_recent", sameTopic: true } : { decision: "queue", reason: "same_topic_stale", sameTopic: true };
64785
65069
  }
65070
+
65071
+ // operator-events.ts
65072
+ init_format();
65073
+
64786
65074
  // raw-error-scrub.ts
64787
65075
  function stripRawErrorBytes(raw) {
64788
65076
  if (typeof raw !== "string" || raw.length === 0)
@@ -65352,7 +65640,9 @@ function renderThrottleEscalationNotice(opts) {
65352
65640
  return `${head}
65353
65641
  ${tail}`;
65354
65642
  }
65643
+
65355
65644
  // operator-events.ts
65645
+ init_format();
65356
65646
  function classifyClaudeError(raw) {
65357
65647
  try {
65358
65648
  return classifyInner(raw);
@@ -65539,6 +65829,26 @@ function formatRelativeTail(deltaMs) {
65539
65829
  const remH = hours % 24;
65540
65830
  return remH > 0 ? `~in ${days}d ${remH}h` : `~in ${days}d`;
65541
65831
  }
65832
+ function formatResetClock(resetAt, tz) {
65833
+ if (resetAt == null)
65834
+ return "";
65835
+ const ms = resetAt.getTime();
65836
+ if (!Number.isFinite(ms))
65837
+ return "";
65838
+ return `${fmtLocalClock(ms, tz)} ${tzAbbrev(ms, tz)}`;
65839
+ }
65840
+ function buildRecommendation(parsed, tz) {
65841
+ switch (parsed.kind) {
65842
+ case "auth":
65843
+ return "\u2192 Re-authenticate this account to continue.";
65844
+ case "quota_wall": {
65845
+ const reset2 = formatResetClock(parsed.resetAt, tz);
65846
+ return reset2 ? `\u2192 Switch to another account, or wait for the quota to reset at ${reset2}.` : "\u2192 Switch to another account, or wait for the quota to reset.";
65847
+ }
65848
+ default:
65849
+ return;
65850
+ }
65851
+ }
65542
65852
  function renderLlmError(parsed, agent, tz, now = new Date) {
65543
65853
  const safeAgent = escapeAgent(agent);
65544
65854
  const emoji = kindEmoji(parsed.kind);
@@ -65548,32 +65858,18 @@ function renderLlmError(parsed, agent, tz, now = new Date) {
65548
65858
  lines.push(`_${resetLine}_`);
65549
65859
  if (parsed.model)
65550
65860
  lines.push(`_model: ${escapeAgent(parsed.model)}_`);
65551
- const text4 = lines.join(`
65552
- `);
65553
- switch (parsed.kind) {
65554
- case "auth":
65555
- return {
65556
- text: text4,
65557
- keyboard: {
65558
- inline_keyboard: [
65559
- [
65560
- { text: "\uD83D\uDD10 Reauth now", callback_data: `op:reauth:${encodeURIComponent(agent)}` },
65561
- { text: "\u274c Dismiss", callback_data: `op:dismiss:${encodeURIComponent(agent)}` }
65562
- ]
65563
- ]
65564
- }
65565
- };
65566
- case "quota_wall":
65567
- return {
65568
- text: text4,
65569
- keyboard: {
65570
- inline_keyboard: [
65571
- [{ text: "\u23f3 Wait", callback_data: `op:dismiss:${encodeURIComponent(agent)}` }]
65572
- ]
65573
- }
65574
- };
65575
- default:
65576
- return { text: text4 };
65861
+ const recommendation2 = buildRecommendation(parsed, tz);
65862
+ if (recommendation2)
65863
+ lines.push(recommendation2);
65864
+ return { text: lines.join(`
65865
+ `) };
65866
+ }
65867
+ function renderLlmErrorSafe(parsed, agent, tz, now = new Date) {
65868
+ try {
65869
+ return renderLlmError(parsed, agent, tz, now);
65870
+ } catch {
65871
+ const safeAgent = escapeAgent(agent);
65872
+ return { text: `${kindEmoji(parsed.kind)} ${parsed.coreText} (**${safeAgent}**)` };
65577
65873
  }
65578
65874
  }
65579
65875
  function kindEmoji(kind) {
@@ -66547,12 +66843,13 @@ function addParagraphSpacers2(text4) {
66547
66843
 
66548
66844
  `))
66549
66845
  return restore2(masked);
66550
- const spacerLine = PARAGRAPH_SPACER2;
66551
- const isBlankLine = (line) => /^[ \t\r\f\v]*$/.test(line);
66846
+ const SP = PARAGRAPH_SPACER2;
66552
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
+ };
66553
66852
  const blockKind = (line) => {
66554
- if (asciiTrim(line) === spacerLine)
66555
- return "spacer";
66556
66853
  if (isFenceOpenLine2(line, placeholder))
66557
66854
  return "fence";
66558
66855
  if (isListItemLine2(line))
@@ -66577,58 +66874,65 @@ function addParagraphSpacers2(text4) {
66577
66874
  const shouldSpaceGap = (above, below) => {
66578
66875
  const a = blockKind(above);
66579
66876
  const b = blockKind(below);
66580
- if (a === "spacer" || b === "spacer")
66581
- return false;
66582
66877
  if (a === b && SAME_KIND_TIGHT.has(a))
66583
66878
  return false;
66584
66879
  return true;
66585
66880
  };
66586
- const lines = masked.split(`
66587
- `);
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
+ }
66588
66891
  const out = [];
66589
- for (let i = 0;i < lines.length; i++) {
66590
- const line = lines[i];
66591
- const isBlank = isBlankLine(line);
66592
- if (isBlank) {
66593
- const prevEmitted = out.length > 0 ? out[out.length - 1] : null;
66594
- const prevIsBlank = prevEmitted != null && isBlankLine(prevEmitted);
66595
- if (!prevIsBlank) {
66596
- const above = lastNonBlank2(out, isBlankLine);
66597
- const below = nextNonBlank2(lines, i + 1, isBlankLine);
66598
- const alreadySpaced = above != null && asciiTrim(above) === spacerLine || below != null && asciiTrim(below) === spacerLine;
66599
- if (!alreadySpaced && above != null && below != null && shouldSpaceGap(above, below)) {
66600
- out.push("");
66601
- out.push(spacerLine);
66602
- out.push("");
66603
- continue;
66604
- }
66605
- }
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);
66606
66909
  }
66607
- out.push(line);
66608
66910
  }
66609
66911
  return restore2(out.join(`
66610
66912
  `));
66611
66913
  }
66612
- function lastNonBlank2(arr, isBlank) {
66613
- for (let i = arr.length - 1;i >= 0; i--) {
66614
- if (!isBlank(arr[i]))
66615
- return arr[i];
66616
- }
66617
- return null;
66618
- }
66619
- function nextNonBlank2(lines, from, isBlank) {
66620
- for (let i = from;i < lines.length; i++) {
66621
- if (!isBlank(lines[i]))
66622
- return lines[i];
66623
- }
66624
- return null;
66625
- }
66626
66914
  function normalizePunctuation2(text4) {
66627
66915
  if (!/[\u2014\u2013\u2022\u00b7]/.test(text4))
66628
66916
  return text4;
66629
66917
  const nonce = Math.random().toString(36).slice(2);
66630
66918
  const { masked, restore: restore2 } = maskCodeRegions2(text4, nonce);
66631
- let out = masked.replace(/(\S)[ \t][\u2014\u2013][ \t](?=(\S))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2014(?=(\w))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2013(?=\w)/g, "$1-");
66919
+ const linkMasks = [];
66920
+ const LINK_MASK_PH = `\x00RML${nonce}_`;
66921
+ const maskedLinks = masked.replace(/(\]\()([^)\n]*)(\))/g, (_m, open, href, close) => {
66922
+ const idx = linkMasks.length;
66923
+ linkMasks.push(href);
66924
+ return `${open}${LINK_MASK_PH}${idx}\x00${close}`;
66925
+ });
66926
+ const maskedAutolinks = maskedLinks.replace(/(<)([a-zA-Z][a-zA-Z0-9+.-]*:[^>\s]*)(>)/g, (_m, lt, uri, gt) => {
66927
+ const idx = linkMasks.length;
66928
+ linkMasks.push(uri);
66929
+ return `${lt}${LINK_MASK_PH}${idx}\x00${gt}`;
66930
+ });
66931
+ const escNonce = nonce.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
66932
+ const linkRestoreRe = new RegExp(`\x00RML${escNonce}_(\\d+)\x00`, "g");
66933
+ const restoreLinks = (s) => s.replace(linkRestoreRe, (_m, idx) => linkMasks[Number(idx)] ?? _m);
66934
+ let out = maskedAutolinks.replace(/(\S)[ \t][\u2014\u2013][ \t](?=(\S))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2014(?=(\w))/g, (_m, a, b) => /\d/.test(a) && /\d/.test(b) ? `${a}-` : `${a}, `).replace(/(\w)\u2013(?=\w)/g, "$1-");
66935
+ out = restoreLinks(out);
66632
66936
  out = out.split(`
66633
66937
  `).map((line) => line.replace(/^([ \t]*)[\u2022\u00b7][ \t]+/, "$1- ")).join(`
66634
66938
  `);
@@ -66821,6 +67125,7 @@ function splitMarkdownChunks2(text4, maxLen = RICH_MESSAGE_MAX_CHARS2) {
66821
67125
  }
66822
67126
  cut = backOffOpenFence2(rest, cut);
66823
67127
  cut = backOffTableRow2(rest, cut);
67128
+ cut = backOffOpenInline2(rest, cut);
66824
67129
  if (cut <= 0) {
66825
67130
  const sliced = hardSliceToCap2(rest, maxLen);
66826
67131
  chunks.push(stripBoundarySpacers2(sliced[0], "trailing"));
@@ -66866,6 +67171,35 @@ function backOffTableRow2(text4, cut) {
66866
67171
  }
66867
67172
  return cut;
66868
67173
  }
67174
+ var INLINE_SPAN_PATTERNS2 = [
67175
+ /`[^`\n]+`/g,
67176
+ /\*\*\*[^*\n]+\*\*\*/g,
67177
+ /___[^_\n]+___/g,
67178
+ /\*\*[^*\n]+\*\*/g,
67179
+ /__[^_\n]+__/g,
67180
+ /(?<![\w*])_[^_\n]+_(?![\w*])/g,
67181
+ /\[[^\]\n]*\]\([^)\n]*\)/g
67182
+ ];
67183
+ function backOffOpenInline2(text4, cut) {
67184
+ if (cut <= 0 || cut >= text4.length)
67185
+ return cut;
67186
+ let earliest = cut;
67187
+ for (const re of INLINE_SPAN_PATTERNS2) {
67188
+ re.lastIndex = 0;
67189
+ let m;
67190
+ while ((m = re.exec(text4)) !== null) {
67191
+ const start = m.index;
67192
+ const end = start + m[0].length;
67193
+ if (start < cut && cut < end && start < earliest)
67194
+ earliest = start;
67195
+ if (start >= cut)
67196
+ break;
67197
+ if (re.lastIndex === start)
67198
+ re.lastIndex = start + 1;
67199
+ }
67200
+ }
67201
+ return earliest;
67202
+ }
66869
67203
 
66870
67204
  // rich-send.ts
66871
67205
  var import_grammy8 = __toESM(require_mod2(), 1);
@@ -66984,6 +67318,7 @@ function scrubVoice2(text4) {
66984
67318
  }
66985
67319
 
66986
67320
  // gateway/outbound-send-path.ts
67321
+ init_format();
66987
67322
  init_text_voice_scrub();
66988
67323
  function normalizeOutboundBody(rawText, site, redact2) {
66989
67324
  let text4 = normalizeParagraphBreaks(repairEscapedWhitespace(rawText));
@@ -67666,7 +68001,7 @@ function isSilentFlushMarker2(text4) {
67666
68001
  return SILENT_MARKERS2.has(trimmed.toUpperCase());
67667
68002
  }
67668
68003
  var TRIVIAL_CONFIRMATIONS2 = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
67669
- function isTrivialConfirmationLine(line) {
68004
+ function isTrivialConfirmationLine2(line) {
67670
68005
  let t = line.trim();
67671
68006
  if (t.length === 0 || t.length > 8)
67672
68007
  return false;
@@ -67674,7 +68009,7 @@ function isTrivialConfirmationLine(line) {
67674
68009
  t = t.slice(0, -1);
67675
68010
  return TRIVIAL_CONFIRMATIONS2.has(t.toUpperCase());
67676
68011
  }
67677
- function isCompositeSilentNoise(text4) {
68012
+ function isCompositeSilentNoise2(text4) {
67678
68013
  if (typeof text4 !== "string")
67679
68014
  return false;
67680
68015
  const lines = text4.split(`
@@ -67684,9 +68019,9 @@ function isCompositeSilentNoise(text4) {
67684
68019
  const hasMarker = lines.some((l) => isSilentFlushMarker2(l));
67685
68020
  if (!hasMarker)
67686
68021
  return false;
67687
- return lines.every((l) => isSilentFlushMarker2(l) || isTrivialConfirmationLine(l));
68022
+ return lines.every((l) => isSilentFlushMarker2(l) || isTrivialConfirmationLine2(l));
67688
68023
  }
67689
- function endsWithSilentMarker(text4) {
68024
+ function endsWithSilentMarker2(text4) {
67690
68025
  if (typeof text4 !== "string")
67691
68026
  return false;
67692
68027
  const lines = text4.split(`
@@ -67695,7 +68030,7 @@ function endsWithSilentMarker(text4) {
67695
68030
  return false;
67696
68031
  return isSilentFlushMarker2(lines[lines.length - 1]);
67697
68032
  }
67698
- function decideTurnFlush(input) {
68033
+ function decideTurnFlush2(input) {
67699
68034
  const flushEnabled = input.flushEnabled !== false;
67700
68035
  if (!flushEnabled)
67701
68036
  return { kind: "skip", reason: "flag-disabled" };
@@ -67710,9 +68045,9 @@ function decideTurnFlush(input) {
67710
68045
  return { kind: "skip", reason: "empty-text" };
67711
68046
  if (isSilentFlushMarker2(joined))
67712
68047
  return { kind: "skip", reason: "silent-marker" };
67713
- if (isCompositeSilentNoise(joined))
68048
+ if (isCompositeSilentNoise2(joined))
67714
68049
  return { kind: "skip", reason: "silent-marker" };
67715
- if (endsWithSilentMarker(joined))
68050
+ if (endsWithSilentMarker2(joined))
67716
68051
  return { kind: "skip", reason: "silent-marker" };
67717
68052
  return { kind: "flush", text: joined };
67718
68053
  }
@@ -67726,6 +68061,72 @@ function isTurnFlushSafetyEnabled(env = process.env) {
67726
68061
  return true;
67727
68062
  }
67728
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
+
67729
68130
  // gateway/turn-end-gate.ts
67730
68131
  function decideTurnEndGate(snapshot) {
67731
68132
  const { flushDecision, finalAnswerDelivered } = snapshot;
@@ -69152,11 +69553,12 @@ async function injectSlashCommand(agentName3, command, opts = {}) {
69152
69553
  session,
69153
69554
  command: command.trim(),
69154
69555
  settleMs,
69155
- timeoutMs
69556
+ timeoutMs,
69557
+ precondition: opts.precondition
69156
69558
  }));
69157
69559
  }
69158
69560
  async function injectSlashCommandWith(runner, args) {
69159
- const { socket, session, command, settleMs, timeoutMs } = args;
69561
+ const { socket, session, command, settleMs, timeoutMs, precondition } = args;
69160
69562
  let bareVerb;
69161
69563
  try {
69162
69564
  bareVerb = validateInjectCommand(command);
@@ -69186,6 +69588,17 @@ async function injectSlashCommandWith(runner, args) {
69186
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.`
69187
69589
  };
69188
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
+ }
69189
69602
  const before = runner.capture(socket, session) ?? "";
69190
69603
  try {
69191
69604
  runner.send(socket, session, ["send-keys", "-l", command]);
@@ -69339,7 +69752,9 @@ function makeTmuxRunner2(tmuxBin) {
69339
69752
  }
69340
69753
  };
69341
69754
  }
69755
+
69342
69756
  // stream-reply-handler.ts
69757
+ init_format();
69343
69758
  function buildAccentHeader(accent) {
69344
69759
  switch (accent) {
69345
69760
  case "in-progress":
@@ -69388,6 +69803,12 @@ _truncated_`;
69388
69803
  }
69389
69804
  return { body: verbHtml, accent: "done" };
69390
69805
  }
69806
+ if (result.outcome === "skipped") {
69807
+ return {
69808
+ body: `${verbHtml} \u2014 skipped (precondition not met at send time)`,
69809
+ accent: "issue"
69810
+ };
69811
+ }
69391
69812
  const code2 = result.errorCode ?? "tmux_failed";
69392
69813
  const msg = result.errorMessage ?? "unknown error";
69393
69814
  if (code2 === "blocked") {
@@ -72070,6 +72491,8 @@ function decideIdleClear(state3, now) {
72070
72491
  return { clear: false };
72071
72492
  if (state3.turnInFlight)
72072
72493
  return { clear: false };
72494
+ if (state3.backgroundWorkInFlight)
72495
+ return { clear: false };
72073
72496
  if (state3.alreadyCleared)
72074
72497
  return { clear: false };
72075
72498
  if (now - lastEventAt(state3) < state3.idleClearMs)
@@ -72081,6 +72504,67 @@ function classifyIdleEvent(kind, durationMs) {
72081
72504
  const synthetic = turnEnded && durationMs === -1;
72082
72505
  return { activity: !synthetic, turnEnded };
72083
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
+ }
72084
72568
  function idleDurationToMs(raw) {
72085
72569
  const m = /^(\d+)([smh])$/.exec(raw.trim());
72086
72570
  if (!m)
@@ -73169,6 +73653,7 @@ function recordWebhookEvent(rec, deps = {}) {
73169
73653
  }
73170
73654
 
73171
73655
  // gateway/ipc-server.ts
73656
+ init_format();
73172
73657
  import { renameSync as renameSync11, unlinkSync as unlinkSync14, chmodSync as chmodSync9 } from "fs";
73173
73658
  var MAX_BUFFER_SIZE = 1024 * 1024;
73174
73659
  var VALID_OPERATOR_KINDS = new Set([
@@ -73874,6 +74359,7 @@ function validateInput(input) {
73874
74359
  }
73875
74360
 
73876
74361
  // gateway/drive-write-approval.ts
74362
+ init_format();
73877
74363
  var DEFAULT_TTL_MS = 5 * 60 * 1000;
73878
74364
  var MAX_TTL_MS = 30 * 60 * 1000;
73879
74365
  var MIN_TTL_MS = 30 * 1000;
@@ -74004,6 +74490,7 @@ function clampTtl(requested, fallback, min, max) {
74004
74490
  }
74005
74491
 
74006
74492
  // gateway/ms365-write-approval.ts
74493
+ init_format();
74007
74494
  function validateMs365Preview(input) {
74008
74495
  if (!input || typeof input !== "object")
74009
74496
  return null;
@@ -74182,6 +74669,7 @@ async function handleRequestMs365Approval(client3, msg, deps) {
74182
74669
  }
74183
74670
 
74184
74671
  // gateway/diff-preview-card.ts
74672
+ init_format();
74185
74673
  var import_grammy10 = __toESM(require_mod2(), 1);
74186
74674
  var REQUEST_ID_RE = /^[0-9a-f]{32}$/;
74187
74675
  var PENDING_FILE_ID_SENTINEL = "pending-create";
@@ -74398,6 +74886,9 @@ function cronIdentity(agent) {
74398
74886
  function isCronIdentity(name) {
74399
74887
  return typeof name === "string" && name.endsWith(CRON_IDENTITY_SUFFIX);
74400
74888
  }
74889
+ function isCronInjectFire(meta) {
74890
+ return meta?.source === "cron" || meta?.session === "cron";
74891
+ }
74401
74892
  function resolveInjectTarget(agentName3, meta) {
74402
74893
  return meta?.session === "cron" ? cronIdentity(agentName3) : agentName3;
74403
74894
  }
@@ -75660,6 +76151,44 @@ function maybeRotate(path2, fs2, maxBytes = TURNS_JSONL_MAX_BYTES) {
75660
76151
  return true;
75661
76152
  }
75662
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
+
75663
76192
  // gateway/inbound-delivery-confirm.ts
75664
76193
  function createDeliveryQueue() {
75665
76194
  return { pending: new Map };
@@ -76340,6 +76869,7 @@ function maybeFireWarmup(ctx) {
76340
76869
 
76341
76870
  // gateway/mental-model-propose-card.ts
76342
76871
  init_approval_card();
76872
+ init_format();
76343
76873
  function renderMentalModelProposeCard(req) {
76344
76874
  const lines = [];
76345
76875
  lines.push(`\uD83E\uDDE0 **${escapeHtmlForTg(req.agent)}** proposes a mental model`);
@@ -78257,25 +78787,81 @@ function redactSecrets(text4) {
78257
78787
  return out;
78258
78788
  }
78259
78789
 
78260
- // narrative-dedup.ts
78261
- var REPLY_TOOLS2 = new Set(["reply", "stream_reply"]);
78262
- function normalizeNarrative2(s) {
78263
- return s.replace(/[*_`>#~]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
78264
- }
78265
- function prefixSimilarity2(a, b) {
78266
- const x = normalizeNarrative2(a);
78267
- const y = normalizeNarrative2(b);
78268
- if (x.length === 0 || y.length === 0)
78269
- return 0;
78270
- const n = Math.min(x.length, y.length);
78271
- let i = 0;
78272
- while (i < n && x[i] === y[i])
78273
- i++;
78274
- return i / n;
78275
- }
78276
- var DRAFT_SUPPRESS_THRESHOLD2 = 0.8;
78277
- function isDraftOfReply2(textBlock, replyText) {
78278
- 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
+ }
78279
78865
  }
78280
78866
 
78281
78867
  // subagent-watcher.ts
@@ -78412,6 +78998,9 @@ var DEFAULT_STALL_THRESHOLD_MS = 60000;
78412
78998
  var DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300000;
78413
78999
  var DEFAULT_SILENT_STALL_TERMINAL_MS = 300000;
78414
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
+ }
78415
79004
  var DEFAULT_DEFERRAL_LOG_INTERVAL_MS = 60000;
78416
79005
  var LONG_RUNNING_TOOLS = new Set(["Bash"]);
78417
79006
  var MAX_RESURRECTIONS = 1;
@@ -78525,6 +79114,8 @@ function backfillJsonlAgentId(db2, jsonlPath, agentId, log) {
78525
79114
  }
78526
79115
  function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, parentStateDir, onUnstall, onFileVanished, onProgress) {
78527
79116
  try {
79117
+ if (entry.narrativeGate != null)
79118
+ entry.narrativeGate.tick(now);
78528
79119
  const stat = fs2.statSync(entry.filePath);
78529
79120
  if (stat.size < tail.cursor) {
78530
79121
  tail.cursor = 0;
@@ -78585,43 +79176,87 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
78585
79176
  entry.errorDetail = errInfo.detail.slice(0, SUBAGENT_RESULT_TEXT_MAX);
78586
79177
  }
78587
79178
  const events = projectSubagentLine(line, entry.agentId, startState);
78588
- const fireNarrativeProgress = () => {
78589
- if (onProgress == null || entry.state !== "running" || entry.historical)
78590
- return false;
78591
- try {
78592
- onProgress({
78593
- agentId: entry.agentId,
78594
- description: entry.description,
78595
- latestSummary: entry.lastResultText,
78596
- elapsedMs: now - entry.dispatchedAt,
78597
- prevBucketIdx: entry.lastProgressBucketIdx,
78598
- setBucketIdx: (b) => {
78599
- entry.lastProgressBucketIdx = b;
78600
- },
78601
- lastTool: entry.lastTool,
78602
- toolCount: entry.toolCount,
78603
- model: entry.currentModel
78604
- });
78605
- return true;
78606
- } catch (cbErr) {
78607
- log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${cbErr.message}`);
78608
- return false;
78609
- }
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
+ };
78610
79252
  };
78611
79253
  const resolvePendingSubNarrative = (toolName, toolInput) => {
78612
- if (entry.pendingNarrative == null)
79254
+ if (entry.narrativeGate == null)
78613
79255
  return false;
78614
- const pending = entry.pendingNarrative;
78615
- entry.pendingNarrative = null;
78616
- if (toolName != null && REPLY_TOOLS2.has(toolName)) {
78617
- const replyText = typeof toolInput?.text === "string" ? toolInput.text : "";
78618
- if (isDraftOfReply2(pending.text, replyText))
78619
- return false;
78620
- } else if (toolName == null && entry.lastReplyText != null && entry.lastReplyText.length > 0) {
78621
- if (isDraftOfReply2(pending.text, entry.lastReplyText))
78622
- return false;
79256
+ if (toolName == null) {
79257
+ return entry.narrativeGate.resolveAtTurnEnd(entry.lastReplyText ?? "");
78623
79258
  }
78624
- return fireNarrativeProgress();
79259
+ return entry.narrativeGate.resolveOnTool(toolName, toolInput);
78625
79260
  };
78626
79261
  for (const ev of events) {
78627
79262
  const idleSecBeforeBump = Math.round((now - entry.lastActivityAt) / 1000);
@@ -78728,10 +79363,9 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
78728
79363
  } else if (ev.kind === "sub_agent_text") {
78729
79364
  entry.lastSummaryLine = clipNarrative(ev.text);
78730
79365
  entry.lastResultText = ev.text.trim().slice(0, SUBAGENT_RESULT_TEXT_MAX);
78731
- if (entry.pendingNarrative != null) {
78732
- fireNarrativeProgress();
78733
- }
78734
- entry.pendingNarrative = { text: ev.text };
79366
+ if (entry.narrativeGate == null)
79367
+ entry.narrativeGate = buildNarrativeGate();
79368
+ entry.narrativeGate.stage(ev.text);
78735
79369
  } else if (ev.kind === "sub_agent_tool_result") {
78736
79370
  if (ev.toolUseId != null && ev.toolUseId !== "") {
78737
79371
  entry.inflightToolUseIds.delete(ev.toolUseId);
@@ -78778,7 +79412,7 @@ function startSubagentWatcher(config) {
78778
79412
  const stallThresholdMs = config.stallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_MS") ?? DEFAULT_STALL_THRESHOLD_MS;
78779
79413
  const silentSynthesisStallThresholdMs = config.silentSynthesisStallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_SILENT_SYNTH_STALL_MS") ?? DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS;
78780
79414
  const silentStallTerminalMs = config.silentStallTerminalMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS") ?? DEFAULT_SILENT_STALL_TERMINAL_MS;
78781
- const inflightTerminalCapMs = config.inflightTerminalCapMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_TERMINAL_CAP_MS") ?? DEFAULT_INFLIGHT_TERMINAL_CAP_MS;
79415
+ const inflightTerminalCapMs = resolveInflightTerminalCapMs(config.inflightTerminalCapMs);
78782
79416
  const deferralLogIntervalMs = config.deferralLogIntervalMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_DEFERRAL_LOG_INTERVAL_MS") ?? DEFAULT_DEFERRAL_LOG_INTERVAL_MS;
78783
79417
  const inflightPromoteMaxAgeMs = config.inflightPromoteMaxAgeMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_MAX_AGE_MS") ?? DEFAULT_INFLIGHT_PROMOTE_MAX_AGE_MS;
78784
79418
  const terminatedAgentIdsCap = config.terminatedAgentIdsCap ?? TERMINATED_AGENT_IDS_CAP;
@@ -78939,7 +79573,8 @@ function startSubagentWatcher(config) {
78939
79573
  }
78940
79574
  entry.toolCount = 0;
78941
79575
  entry.lastTool = null;
78942
- entry.pendingNarrative = null;
79576
+ entry.narrativeGate?.reset();
79577
+ entry.narrativeGate = null;
78943
79578
  tail.cursor = 0;
78944
79579
  tail.pendingPartial = "";
78945
79580
  tail.hasEmittedStart = false;
@@ -79038,6 +79673,13 @@ function startSubagentWatcher(config) {
79038
79673
  }
79039
79674
  terminatedAgentIds.add(agentId);
79040
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
+ }
79041
79683
  }
79042
79684
  function recordFalseFinish(agentId, filePath, n) {
79043
79685
  let size = 0;
@@ -82081,10 +82723,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
82081
82723
  }
82082
82724
 
82083
82725
  // ../src/build-info.ts
82084
- var VERSION = "0.18.18";
82085
- var COMMIT_SHA = "a256b3b1";
82086
- var COMMIT_DATE = "2026-07-13T03:10:38Z";
82087
- var LATEST_PR = 3210;
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;
82088
82730
  var COMMITS_AHEAD_OF_TAG = 0;
82089
82731
 
82090
82732
  // gateway/boot-version.ts
@@ -82341,6 +82983,7 @@ async function listGrantsViaBroker2(agent, opts) {
82341
82983
  }
82342
82984
 
82343
82985
  // gateway/linear-activity.ts
82986
+ init_format();
82344
82987
  init_client();
82345
82988
 
82346
82989
  // ../src/linear/oauth-refresh.ts
@@ -84508,6 +85151,7 @@ function resolveSubagentOriginChat(agentId) {
84508
85151
  }
84509
85152
  }
84510
85153
  var WORKER_FEED_FALLBACK_LOG_CAP = 256;
85154
+ var WORKER_FEED_STALE_TTL_MARGIN_MS = 300000;
84511
85155
  var workerFeedOwnerDmFallbackLogged = new Set;
84512
85156
  function resolveWorkerFeedChat(agentId, fleetChatId) {
84513
85157
  const origin = resolveSubagentOriginChat(agentId);
@@ -85405,14 +86049,14 @@ function releaseTurnBufferGate(key, endingTurn) {
85405
86049
  }
85406
86050
  function emitTurnRecord(turn, endedAt) {
85407
86051
  try {
85408
- const rec = JSON.stringify({
85409
- ts: Math.floor(endedAt / 1000),
86052
+ const rec = JSON.stringify(buildTurnRecord({
85410
86053
  agent: process.env.SWITCHROOM_AGENT_NAME ?? "unknown",
85411
- duration_ms: turn.startedAt > 0 ? endedAt - turn.startedAt : 0,
85412
- tools: turn.toolCallCount ?? 0,
85413
- status: turn.finalAnswerDelivered ? "complete" : "no_reply",
85414
- turn_id: turn.turnId
85415
- }) + `
86054
+ startedAt: turn.startedAt,
86055
+ toolCallCount: turn.toolCallCount ?? 0,
86056
+ turnId: turn.turnId,
86057
+ finalAnswerDelivered: turn.finalAnswerDelivered,
86058
+ deliveryOutcome: turn.deliveryOutcome
86059
+ }, endedAt)) + `
85416
86060
  `;
85417
86061
  const turnsPath = "/state/agent/turns.jsonl";
85418
86062
  maybeRotate(turnsPath, {
@@ -85428,15 +86072,18 @@ function emitTurnRecord(turn, endedAt) {
85428
86072
  appendFileSync6(turnsPath, rec);
85429
86073
  } catch {}
85430
86074
  }
85431
- function endCurrentTurnAtomic(turn) {
86075
+ function endCurrentTurnAtomic(turn, opts) {
85432
86076
  const key = statusKey(turn.sessionChatId, turn.sessionThreadId);
85433
86077
  if (!turnLiveForItsTopic(turn))
85434
- return;
86078
+ return null;
86079
+ clearAnswerReadyFlushTimeout(turn);
85435
86080
  endCurrentTurnForKey(turn, key);
85436
86081
  const turnEndedAt = Date.now();
85437
86082
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("clear", "turn_end", turn, turnEndedAt)}
85438
86083
  `);
85439
- emitTurnRecord(turn, turnEndedAt);
86084
+ if (opts?.deferRecord !== true) {
86085
+ emitTurnRecord(turn, turnEndedAt);
86086
+ }
85440
86087
  const degraded = detectStatusSurfaceDegraded(turn);
85441
86088
  if (degraded != null) {
85442
86089
  process.stderr.write(`telegram gateway: status-surface DEGRADED reason=${degraded.reason} turnId=${turn.turnId} chat=${turn.sessionChatId} thread=${turn.sessionThreadId ?? "-"} ${degraded.detail}
@@ -85453,8 +86100,10 @@ function endCurrentTurnAtomic(turn) {
85453
86100
  clearTimeout(turn.noReplyDrainTimer);
85454
86101
  turn.noReplyDrainTimer = null;
85455
86102
  }
86103
+ turn.narrativeGate?.teardown();
85456
86104
  purgeReactionTracking(statusKey(turn.sessionChatId, turn.sessionThreadId), turn);
85457
86105
  armNoReplyDrainTimer(turn);
86106
+ return turnEndedAt;
85458
86107
  }
85459
86108
  function maybeProactiveCompact() {
85460
86109
  if (compactDispatching)
@@ -85514,16 +86163,9 @@ function maybeProactiveCompact() {
85514
86163
  compactDispatching = false;
85515
86164
  });
85516
86165
  }
85517
- var lastIdleActivityAt = Date.now();
85518
- var lastIdleTurnEndAt = null;
85519
- var idleAutoCleared = false;
85520
- var idleClearDispatching = false;
86166
+ var idleTracker = new IdleTracker(Date.now());
85521
86167
  function markIdleActivity() {
85522
- lastIdleActivityAt = Date.now();
85523
- idleAutoCleared = false;
85524
- }
85525
- function markIdleTurnEnd() {
85526
- lastIdleTurnEndAt = Date.now();
86168
+ idleTracker.noteInbound(Date.now());
85527
86169
  }
85528
86170
  function resolveIdleClearMs() {
85529
86171
  const env = process.env.SWITCHROOM_IDLE_CLEAR_MS;
@@ -85548,30 +86190,39 @@ function resolveIdleClearMs() {
85548
86190
  }
85549
86191
  }
85550
86192
  function maybeIdleClear() {
85551
- if (idleClearDispatching)
86193
+ if (idleTracker.isDispatching)
85552
86194
  return;
85553
86195
  const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
85554
86196
  if (!agentName3)
85555
86197
  return;
85556
86198
  const idleClearMs = resolveIdleClearMs();
85557
- const decision = decideIdleClear({
85558
- lastActivityAt: lastIdleActivityAt,
85559
- lastTurnEndedAt: lastIdleTurnEndAt,
86199
+ const decision = idleTracker.decide(Date.now(), {
85560
86200
  idleClearMs,
85561
- alreadyCleared: idleAutoCleared,
85562
- turnInFlight: turnInFlightForGate()
85563
- }, Date.now());
86201
+ turnInFlight: turnInFlightForGate(),
86202
+ backgroundWorkInFlight: anyPendingAsyncDispatchWithin(BACKGROUND_WORK_SUPPRESS_TTL_MS)
86203
+ });
85564
86204
  if (!decision.clear)
85565
86205
  return;
85566
- idleAutoCleared = true;
85567
- idleClearDispatching = true;
86206
+ idleTracker.markClearFired();
86207
+ idleTracker.beginDispatch();
85568
86208
  process.stderr.write(`telegram gateway: idle auto-/clear for ${agentName3} (idle >= ${Math.round(idleClearMs / 60000)}m)
85569
86209
  `);
85570
- 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) => {
85571
86222
  process.stderr.write(`telegram gateway: idle /clear inject failed for ${agentName3}: ${err instanceof Error ? err.message : String(err)}
85572
86223
  `);
85573
86224
  }).finally(() => {
85574
- idleClearDispatching = false;
86225
+ idleTracker.endDispatch();
85575
86226
  });
85576
86227
  }
85577
86228
  async function postCompactCard(occ, cap) {
@@ -86768,6 +87419,7 @@ var inboundCoalescer = createInboundCoalescer({
86768
87419
  });
86769
87420
  function emitGatewayOperatorEvent(event) {
86770
87421
  const { agent, kind } = event;
87422
+ event = { ...event, detail: redactOutboundText(event.detail, "operator_event") };
86771
87423
  let throttleEscalation = null;
86772
87424
  let escalationFired = false;
86773
87425
  let rateLimitedCooldownConsulted = false;
@@ -86872,9 +87524,9 @@ function emitGatewayOperatorEvent(event) {
86872
87524
  return;
86873
87525
  }
86874
87526
  const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
86875
- const r = renderLlmError(parsed, agent, tz, new Date(now));
87527
+ const r = renderLlmErrorSafe(parsed, agent, tz, new Date(now));
86876
87528
  renderedText = r.text;
86877
- renderedKeyboard = r.keyboard;
87529
+ renderedKeyboard = undefined;
86878
87530
  } else {
86879
87531
  try {
86880
87532
  const r = renderOperatorEvent(event);
@@ -86950,6 +87602,7 @@ var STREAM_THROTTLE_MS_OVERRIDE = (() => {
86950
87602
  return Number.isFinite(n) && n >= 0 ? n : undefined;
86951
87603
  })();
86952
87604
  var TURN_FLUSH_SAFETY_ENABLED = isTurnFlushSafetyEnabled();
87605
+ var ANSWER_READY_FLUSH_MS2 = resolveAnswerReadyFlushMs(process.env);
86953
87606
  var ANSWER_STREAM_VISIBLE_ENABLED = parseVisibleAnswerStreamEnabled(process.env.SWITCHROOM_VISIBLE_ANSWER_STREAM);
86954
87607
  var ANSWER_LANE = resolveAnswerLaneConfig({
86955
87608
  visibleEnabled: ANSWER_STREAM_VISIBLE_ENABLED
@@ -87429,6 +88082,7 @@ var SILENCE_FALLBACK_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FALLBACK_MS", 3
87429
88082
  var SILENCE_FALLBACK_HARD_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FALLBACK_HARD_MS", 900000);
87430
88083
  var SILENCE_FLOOR_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FLOOR_MS", 45000);
87431
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";
87432
88086
  var SILENCE_DEFER_INFLIGHT_TOOLS = process.env.SWITCHROOM_SILENCE_DEFER_INFLIGHT_TOOLS === "1";
87433
88087
  var SILENCE_LIVENESS_PRODUCTION = process.env.SWITCHROOM_SILENCE_LIVENESS_PRODUCTION !== "0";
87434
88088
  var MEMORY_LEGIBILITY_ENABLED = isMemoryLegibilityEnabled(process.env.SWITCHROOM_MEMORY_LEGIBILITY);
@@ -87743,6 +88397,103 @@ function agentHasInFlightBackgroundWork(now) {
87743
88397
  return ageMs != null && ageMs < TURN_ACTIVE_MARKER_FRESH_MS;
87744
88398
  }
87745
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
+ }
87746
88497
  function obligationSweep() {
87747
88498
  if (!OBLIGATION_LEDGER_ENABLED)
87748
88499
  return;
@@ -88520,7 +89271,8 @@ var ipcServer = createIpcServer({
88520
89271
  swallowingApiCall(() => bot.api.editMessageText(operator, msg.messageId, richMessage2(msg.text), {}), { chat_id: String(operator), verb: "rollout-status-edit" });
88521
89272
  },
88522
89273
  onInjectInbound(_client, msg) {
88523
- markIdleActivity();
89274
+ if (!isCronInjectFire(msg.inbound.meta))
89275
+ markIdleActivity();
88524
89276
  const promptKey = typeof msg.inbound.meta?.prompt_key === "string" ? msg.inbound.meta.prompt_key : "unknown";
88525
89277
  const source = typeof msg.inbound.meta?.source === "string" ? msg.inbound.meta.source : "unknown";
88526
89278
  const isDurableReplay = inboundSpool != null && typeof msg.inbound.meta?.replay_fire_ms === "string" && msg.inbound.meta.replay_fire_ms.length > 0;
@@ -90748,6 +91500,33 @@ function resetOrphanedReplyTimeout() {
90748
91500
  }, ORPHANED_REPLY_TIMEOUT_MS);
90749
91501
  }
90750
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
+ }
90751
91530
  function closeActivityLane(chatId, threadId) {
90752
91531
  const key = chatKeyWithSuffix2(chatId, threadId, "activity");
90753
91532
  const stream = activeDraftStreams.get(key);
@@ -90781,6 +91560,45 @@ function composeTurnActivity(turn, final = false, liveSuffix = "") {
90781
91560
  };
90782
91561
  return renderActivityFeedWithNested2(turn.mirrorLines, childLines, final, liveSuffix, stepCount, header);
90783
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
+ }
90784
91602
  function showNarrativeStep(turn, text5) {
90785
91603
  const rendered = appendActivityLabel(turn.mirrorLines, clipNarrative2(text5));
90786
91604
  if (rendered == null)
@@ -90796,31 +91614,13 @@ function showNarrativeStep(turn, text5) {
90796
91614
  });
90797
91615
  }
90798
91616
  function resolvePendingNarrativeOnTool(turn, toolName, input) {
90799
- const pending2 = turn.pendingNarrative;
90800
- if (pending2 == null)
90801
- return;
90802
- turn.pendingNarrative = null;
90803
- if (REPLY_TOOLS.has(toolName)) {
90804
- const replyText = typeof input?.text === "string" ? input.text : "";
90805
- if (isDraftOfReply(pending2.text, replyText))
90806
- return;
90807
- }
90808
- showNarrativeStep(turn, pending2.text);
91617
+ turn.narrativeGate.resolveOnTool(toolName, input);
90809
91618
  }
90810
91619
  function stagePendingNarrative(turn, text5) {
90811
- if (turn.pendingNarrative != null) {
90812
- showNarrativeStep(turn, turn.pendingNarrative.text);
90813
- }
90814
- turn.pendingNarrative = { text: text5 };
91620
+ turn.narrativeGate.stage(text5);
90815
91621
  }
90816
91622
  function flushPendingNarrativeAtTurnEnd(turn, lastReplyText) {
90817
- const pending2 = turn.pendingNarrative;
90818
- if (pending2 == null)
90819
- return;
90820
- turn.pendingNarrative = null;
90821
- if (lastReplyText.length > 0 && isDraftOfReply(pending2.text, lastReplyText))
90822
- return;
90823
- showNarrativeStep(turn, pending2.text);
91623
+ turn.narrativeGate.flushAtTurnEnd(lastReplyText);
90824
91624
  }
90825
91625
  async function drainActivitySummary(turn, producer = "tool", openFlags) {
90826
91626
  try {
@@ -91160,11 +91960,7 @@ function handleSessionEvent(ev) {
91160
91960
  }
91161
91961
  {
91162
91962
  const durationMs = ev.kind === "turn_end" ? ev.durationMs : undefined;
91163
- const signal = classifyIdleEvent(ev.kind, durationMs);
91164
- if (signal.activity)
91165
- markIdleActivity();
91166
- if (signal.turnEnded)
91167
- markIdleTurnEnd();
91963
+ idleTracker.noteEvent(ev.kind, Date.now(), durationMs);
91168
91964
  }
91169
91965
  switch (ev.kind) {
91170
91966
  case "enqueue": {
@@ -91184,6 +91980,7 @@ function handleSessionEvent(ev) {
91184
91980
  clearTimeout(prior.orphanedReplyTimeoutId);
91185
91981
  prior.orphanedReplyTimeoutId = null;
91186
91982
  }
91983
+ prior?.narrativeGate?.teardown();
91187
91984
  const startedAt = Date.now();
91188
91985
  const enqThreadIdNum = ev.threadId != null ? Number(ev.threadId) : undefined;
91189
91986
  const turnId = deriveTurnId(ev.chatId, enqThreadIdNum ?? null, ev.messageId) ?? `${chatKey2(ev.chatId, enqThreadIdNum ?? null)}#synthetic-${startedAt}`;
@@ -91209,6 +92006,7 @@ function handleSessionEvent(ev) {
91209
92006
  silentAnchorText: "",
91210
92007
  capturedText: [],
91211
92008
  orphanedReplyTimeoutId: null,
92009
+ answerReadyFlushTimeoutId: null,
91212
92010
  liveness: new LivenessTracker(startedAt),
91213
92011
  turnId,
91214
92012
  registryKey: null,
@@ -91224,13 +92022,14 @@ function handleSessionEvent(ev) {
91224
92022
  activityEverOpened: false,
91225
92023
  activityDrainFailures: 0,
91226
92024
  mirrorLines: [],
91227
- pendingNarrative: null,
92025
+ narrativeGate: undefined,
91228
92026
  lastReplyText: "",
91229
92027
  foregroundSubAgents: new Map,
91230
92028
  answerStream: null,
91231
92029
  isDm: isDmChatId2(ev.chatId),
91232
92030
  emissionAuthority: new EmissionAuthority(statusKey(ev.chatId, enqThreadIdNum))
91233
92031
  };
92032
+ next.narrativeGate = makeNarrativeGate(next);
91234
92033
  setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
91235
92034
  scheduleEarlyLivenessOpen(next);
91236
92035
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
@@ -91302,6 +92101,7 @@ function handleSessionEvent(ev) {
91302
92101
  const turn = currentTurn;
91303
92102
  if (turn == null)
91304
92103
  return;
92104
+ clearAnswerReadyFlushTimeout(turn);
91305
92105
  resolvePendingNarrativeOnTool(turn, ev.toolName, ev.input);
91306
92106
  turn.toolCallCount++;
91307
92107
  touchTurnActiveMarker2(STATE_DIR);
@@ -91336,6 +92136,7 @@ function handleSessionEvent(ev) {
91336
92136
  const turn = currentTurn;
91337
92137
  if (turn == null)
91338
92138
  return;
92139
+ clearAnswerReadyFlushTimeout(turn);
91339
92140
  resetOrphanedReplyTimeout();
91340
92141
  if (isTelegramSurfaceTool2(ev.toolName))
91341
92142
  return;
@@ -91445,6 +92246,7 @@ function handleSessionEvent(ev) {
91445
92246
  preambleSuppressor.onText(ev.text);
91446
92247
  }
91447
92248
  resetOrphanedReplyTimeout();
92249
+ resetAnswerReadyFlushTimeout();
91448
92250
  if (isContextExhaustionText(ev.text) && turn != null) {
91449
92251
  const chatId = turn.sessionChatId;
91450
92252
  const threadId = turn.sessionThreadId;
@@ -91496,7 +92298,7 @@ function handleSessionEvent(ev) {
91496
92298
  return;
91497
92299
  }
91498
92300
  case "turn_end": {
91499
- if (ev.durationMs === -1) {
92301
+ if (ev.durationMs === -1 && ev.reason !== "answer-ready-quiescence") {
91500
92302
  const turn = currentTurn;
91501
92303
  const key = turn != null ? statusKey(turn.sessionChatId, turn.sessionThreadId) : "";
91502
92304
  const recentlyStreaming = turn != null && turn.liveness.recentlyStreaming(Date.now(), ORPHANED_REPLY_STREAM_WINDOW_MS);
@@ -91570,7 +92372,7 @@ function handleSessionEvent(ev) {
91570
92372
  const chatId = turn.sessionChatId;
91571
92373
  const threadId = turn.sessionThreadId;
91572
92374
  const ctrl = activeStatusReactions.get(statusKey(chatId, threadId));
91573
- const flushDecision = streamFinalizedAsAnswer ? { kind: "skip", reason: "reply-called" } : decideTurnFlush({
92375
+ const flushDecision = streamFinalizedAsAnswer ? { kind: "skip", reason: "reply-called" } : decideTurnFlush2({
91574
92376
  chatId: turn.sessionChatId,
91575
92377
  replyCalled: turn.replyCalled,
91576
92378
  capturedText: turn.capturedText,
@@ -91680,7 +92482,7 @@ function handleSessionEvent(ev) {
91680
92482
  }) ?? { wasEmitted: false, turnKey: null };
91681
92483
  const backstopCardMessageId = cardTakeover.wasEmitted && cardTakeover.turnKey != null ? getPinnedProgressCardMessageId?.(cardTakeover.turnKey) ?? null : null;
91682
92484
  const backstopCardTurnKey = cardTakeover.turnKey;
91683
- endCurrentTurnAtomic(turn);
92485
+ const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true });
91684
92486
  preambleSuppressor.dropNow();
91685
92487
  {
91686
92488
  const tKey = statusKey(chatId, threadId);
@@ -91697,6 +92499,10 @@ function handleSessionEvent(ev) {
91697
92499
  if (recentCount > 0) {
91698
92500
  process.stderr.write(`telegram gateway: turn-flush suppressed \u2014 reply tool sent ${recentCount} message(s) within 2s
91699
92501
  `);
92502
+ if (backstopTurnEndedAt != null) {
92503
+ turn.deliveryOutcome = "suppressed";
92504
+ emitTurnRecord(turn, backstopTurnEndedAt);
92505
+ }
91700
92506
  return;
91701
92507
  }
91702
92508
  } catch {}
@@ -91708,10 +92514,12 @@ function handleSessionEvent(ev) {
91708
92514
  link_preview_options: { is_disabled: true }
91709
92515
  };
91710
92516
  const limit = RICH_MESSAGE_MAX_CHARS2;
91711
- const renderedText = addParagraphSpacers2(capturedText);
91712
- const htmlChunks = splitMarkdownChunks2(renderedText, limit);
92517
+ let htmlChunks = [];
91713
92518
  const sentIds = [];
92519
+ let sendThrew = false;
91714
92520
  try {
92521
+ const renderedText = addParagraphSpacers2(capturedText);
92522
+ htmlChunks = splitMarkdownChunks2(renderedText, limit);
91715
92523
  let firstSendUsedEdit = false;
91716
92524
  let liveThreadId = backstopThreadId;
91717
92525
  if (backstopCardMessageId != null && htmlChunks.length > 0) {
@@ -91774,10 +92582,20 @@ function handleSessionEvent(ev) {
91774
92582
  unpinProgressCardForChat?.(backstopChatId, backstopThreadId);
91775
92583
  }
91776
92584
  } catch (err) {
92585
+ sendThrew = true;
91777
92586
  process.stderr.write(`telegram gateway: turn-flush send failed: ${err.message}
91778
92587
  `);
91779
92588
  if (backstopCtrl)
91780
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
+ }
91781
92599
  }
91782
92600
  })();
91783
92601
  return;
@@ -91819,21 +92637,40 @@ function handleSessionEvent(ev) {
91819
92637
  ended_via: outboundMetrics.outboundCount > 0 ? "reply" : "silent"
91820
92638
  });
91821
92639
  if (turnEndDecision === "reprompt") {
91822
- const silentEndDeps = HISTORY_ENABLED ? {
91823
- hasOutboundDeliveredSince: (cid, sinceMs, tid) => hasOutboundDeliveredSince(cid, sinceMs, tid, 1)
91824
- } : undefined;
91825
- const silentEnd = recordUndeliveredTurnEnd({
91826
- chatId,
91827
- threadId: threadId ?? null,
91828
- turnKey: tKey
91829
- }, silentEndDeps);
91830
- if (silentEnd.exhausted) {
91831
- 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)
91832
- `);
91833
- 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) => {
91834
- 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)
91835
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
91836
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
+ }
91837
92674
  }
91838
92675
  }
91839
92676
  clear(tKey);
@@ -98314,6 +99151,7 @@ var didOneTimeSetup = false;
98314
99151
  },
98315
99152
  floodWaitRemainingMs: probeFloodWaitRemainingMs,
98316
99153
  maxRows: workerFeedMaxRows,
99154
+ staleWorkerTtlMs: resolveInflightTerminalCapMs() + WORKER_FEED_STALE_TTL_MARGIN_MS,
98317
99155
  reconcilePin: ({ feedKey, chatId, messageId }) => {
98318
99156
  if (!PIN_STATUS_WHILE_WORKING)
98319
99157
  return;
@@ -98359,6 +99197,14 @@ var didOneTimeSetup = false;
98359
99197
  process.stderr.write(`telegram gateway: worker ${agentId} NAMED AS LOST \u2014 falsely finalised twice, resurrection chain bound reached (issue #3023)
98360
99198
  `);
98361
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
+ },
98362
99208
  onFinish: ({ agentId, outcome, description, resultText, toolCount, durationMs, background: entryBackground }) => {
98363
99209
  deferredDoneReactions.promote();
98364
99210
  let fleetChatId = "";