switchroom 0.18.19 → 0.18.21

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 (57) hide show
  1. package/dist/cli/ms-365-write-pretool.mjs +92 -20
  2. package/dist/cli/switchroom.js +59 -6
  3. package/dist/host-control/main.js +1 -1
  4. package/package.json +1 -1
  5. package/profiles/_shared/delegation-golden-rule.md.hbs +9 -0
  6. package/profiles/_shared/dev-protocol.md.hbs +2 -0
  7. package/profiles/_shared/execution-discipline.md.hbs +2 -2
  8. package/profiles/coding/CLAUDE.md.hbs +1 -1
  9. package/telegram-plugin/answer-ready-flush.ts +187 -0
  10. package/telegram-plugin/dist/gateway/gateway.js +1114 -184
  11. package/telegram-plugin/format.ts +179 -20
  12. package/telegram-plugin/gateway/cron-session.ts +32 -0
  13. package/telegram-plugin/gateway/gateway.ts +794 -106
  14. package/telegram-plugin/gateway/idle-clear.ts +170 -0
  15. package/telegram-plugin/gateway/inject-handler.ts +11 -0
  16. package/telegram-plugin/gateway/outbound-send-path.ts +9 -9
  17. package/telegram-plugin/gateway/subagent-progress-inbound-builder.ts +17 -0
  18. package/telegram-plugin/gateway/turn-record-status.ts +134 -0
  19. package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +23 -0
  20. package/telegram-plugin/hooks/silent-end-scan.mjs +98 -8
  21. package/telegram-plugin/narrative-flush.ts +181 -0
  22. package/telegram-plugin/pending-work-progress.ts +65 -1
  23. package/telegram-plugin/registry/subagents-schema.ts +6 -0
  24. package/telegram-plugin/session-tail.ts +6 -1
  25. package/telegram-plugin/silent-end.ts +182 -0
  26. package/telegram-plugin/stream-reply-handler.ts +14 -5
  27. package/telegram-plugin/subagent-watcher.ts +330 -82
  28. package/telegram-plugin/tests/answer-ready-flush.test.ts +343 -0
  29. package/telegram-plugin/tests/cron-inject-idle-clock.test.ts +54 -0
  30. package/telegram-plugin/tests/emission-authority-facade.test.ts +13 -10
  31. package/telegram-plugin/tests/format-consistency.test.ts +54 -34
  32. package/telegram-plugin/tests/formatting-parse-regression.test.ts +6 -5
  33. package/telegram-plugin/tests/formatting-torture-set.ts +1 -1
  34. package/telegram-plugin/tests/idle-clear.test.ts +315 -37
  35. package/telegram-plugin/tests/narrative-flush.test.ts +213 -0
  36. package/telegram-plugin/tests/narrative-splice-before-finalize.test.ts +167 -0
  37. package/telegram-plugin/tests/nested-worker-visibility-harness.test.ts +20 -0
  38. package/telegram-plugin/tests/outbound-send-path.test.ts +5 -4
  39. package/telegram-plugin/tests/paragraph-normalizer.test.ts +100 -42
  40. package/telegram-plugin/tests/paragraph-spacer-golden.test.ts +150 -0
  41. package/telegram-plugin/tests/per-topic-current-turn.test.ts +4 -1
  42. package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +194 -0
  43. package/telegram-plugin/tests/silent-end.test.ts +296 -0
  44. package/telegram-plugin/tests/stream-reply-handler.test.ts +12 -9
  45. package/telegram-plugin/tests/subagent-progress-inbound-builder.test.ts +30 -0
  46. package/telegram-plugin/tests/subagent-watcher-first-paint-independence.test.ts +171 -0
  47. package/telegram-plugin/tests/subagent-watcher-narrative-early-paint.test.ts +220 -0
  48. package/telegram-plugin/tests/subagent-watcher.test.ts +13 -12
  49. package/telegram-plugin/tests/telegram-format.test.ts +36 -23
  50. package/telegram-plugin/tests/turn-flush-safety.test.ts +21 -17
  51. package/telegram-plugin/tests/turn-record-status.test.ts +119 -0
  52. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +218 -1
  53. package/telegram-plugin/tests/worker-feed-terminal-cleanup.test.ts +254 -0
  54. package/telegram-plugin/tests/worker-feed-terminal-state-truthful.test.ts +165 -0
  55. package/telegram-plugin/tool-activity-summary.ts +78 -16
  56. package/telegram-plugin/turn-flush-safety.ts +4 -4
  57. package/telegram-plugin/worker-activity-feed.ts +181 -30
@@ -6824,6 +6824,85 @@ function hardenCardBreaks(text) {
6824
6824
  }
6825
6825
  return restore(pieces.join(""));
6826
6826
  }
6827
+ function addParagraphSpacers(text) {
6828
+ if (!text.includes(`
6829
+
6830
+ `))
6831
+ return text;
6832
+ const nonce = Math.random().toString(36).slice(2);
6833
+ const { masked, restore, placeholder } = maskCodeRegions(text, nonce);
6834
+ if (!masked.includes(`
6835
+
6836
+ `))
6837
+ return restore(masked);
6838
+ const SP = PARAGRAPH_SPACER;
6839
+ const asciiTrim = (line) => line.replace(/^[ \t\r\f\v]+|[ \t\r\f\v]+$/g, "");
6840
+ const isBlankish = (line) => {
6841
+ const t = asciiTrim(line);
6842
+ return t === "" || t === SP;
6843
+ };
6844
+ const blockKind = (line) => {
6845
+ if (isFenceOpenLine(line, placeholder))
6846
+ return "fence";
6847
+ if (isListItemLine(line))
6848
+ return "list";
6849
+ if (isTableRowLine(line) || isTableDelimiterLine(line))
6850
+ return "table";
6851
+ if (isBlockquoteLine(line))
6852
+ return "quote";
6853
+ if (isHeadingLine(line))
6854
+ return "heading";
6855
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trimStart()))
6856
+ return "divider";
6857
+ return "prose";
6858
+ };
6859
+ const SAME_KIND_TIGHT = new Set([
6860
+ "list",
6861
+ "table",
6862
+ "quote",
6863
+ "fence",
6864
+ "divider"
6865
+ ]);
6866
+ const shouldSpaceGap = (above, below) => {
6867
+ const a = blockKind(above);
6868
+ const b = blockKind(below);
6869
+ if (a === b && SAME_KIND_TIGHT.has(a))
6870
+ return false;
6871
+ return true;
6872
+ };
6873
+ const toks = [];
6874
+ for (const line of masked.split(`
6875
+ `)) {
6876
+ const kind = isBlankish(line) ? "gap" : "content";
6877
+ const last = toks[toks.length - 1];
6878
+ if (last && last.kind === kind)
6879
+ last.lines.push(line);
6880
+ else
6881
+ toks.push({ kind, lines: [line] });
6882
+ }
6883
+ const out = [];
6884
+ for (let i = 0;i < toks.length; i++) {
6885
+ const tok = toks[i];
6886
+ if (tok.kind === "content") {
6887
+ out.push(...tok.lines);
6888
+ continue;
6889
+ }
6890
+ const prev = toks[i - 1];
6891
+ const next = toks[i + 1];
6892
+ if (prev?.kind === "content" && next?.kind === "content") {
6893
+ const above = prev.lines[prev.lines.length - 1];
6894
+ const below = next.lines[0];
6895
+ if (shouldSpaceGap(above, below))
6896
+ out.push("", SP, "");
6897
+ else
6898
+ out.push("");
6899
+ } else {
6900
+ out.push(...tok.lines);
6901
+ }
6902
+ }
6903
+ return restore(out.join(`
6904
+ `));
6905
+ }
6827
6906
  function normalizePunctuation(text) {
6828
6907
  if (!/[\u2014\u2013\u2022\u00b7]/.test(text))
6829
6908
  return text;
@@ -7051,10 +7130,11 @@ function splitMarkdownChunks(text, maxLen = RICH_MESSAGE_MAX_CHARS) {
7051
7130
  return chunks.map((c) => stripBoundarySpacers(c, "trailing"));
7052
7131
  }
7053
7132
  function stripBoundarySpacers(chunk, side) {
7133
+ const sp = PARAGRAPH_SPACER;
7054
7134
  if (side === "leading") {
7055
- return chunk.replace(/^(?:[ \t]*\n)+/, "");
7135
+ return chunk.replace(new RegExp(`^(?:[ \\t]*${sp}?[ \\t]*\\n+)+`), "");
7056
7136
  }
7057
- return chunk.replace(/(?:\n[ \t]*)+$/, "");
7137
+ return chunk.replace(new RegExp(`(?:\\n+[ \\t]*${sp}?[ \\t]*)+$`), "");
7058
7138
  }
7059
7139
  function backOffOpenFence(text, cut) {
7060
7140
  if (cut <= 0 || cut >= text.length)
@@ -7103,7 +7183,7 @@ function backOffOpenInline(text, cut) {
7103
7183
  }
7104
7184
  return earliest;
7105
7185
  }
7106
- var RICH_MESSAGE_MAX_CHARS = 32768, INLINE_SPAN_PATTERNS;
7186
+ var RICH_MESSAGE_MAX_CHARS = 32768, PARAGRAPH_SPACER = "\u00a0", INLINE_SPAN_PATTERNS;
7107
7187
  var init_format = __esm(() => {
7108
7188
  INLINE_SPAN_PATTERNS = [
7109
7189
  /`[^`\n]+`/g,
@@ -40217,27 +40297,43 @@ function renderActivityFeedWithNested(lines, childLines, final = false, liveSuff
40217
40297
  });
40218
40298
  }
40219
40299
  var COMBINED_ROW_DESC_MAX = 72;
40300
+ var MAX_COMBINED_BODY_LINES = 13;
40301
+ var PER_WORKER_HEADER_COST = 1;
40302
+ function combinedHistoryDepth(w) {
40303
+ if (w <= 0)
40304
+ return 1;
40305
+ const raw = Math.floor((MAX_COMBINED_BODY_LINES - PER_WORKER_HEADER_COST * w) / w);
40306
+ return Math.max(1, Math.min(STATUS_ROLLING_LINES, raw));
40307
+ }
40220
40308
  function renderCombinedWorkerFeed(rows, opts) {
40221
40309
  if (rows.length === 0)
40222
40310
  return null;
40223
40311
  const maxRows = Math.max(1, Math.floor(opts.maxRows));
40224
- const rowLines = (r) => {
40312
+ const rowHeader = (r) => {
40225
40313
  const desc = escapeMarkdown(truncate(stripMarkdown(r.description).replace(/\s+/g, " ").trim() || "background task", COMBINED_ROW_DESC_MAX));
40226
40314
  const toolWord = r.toolCount === 1 ? "tool" : "tools";
40227
40315
  const modelLabel = formatModelLabel(r.model);
40228
40316
  const modelPart = modelLabel != null ? ` \u00b7 ${escapeMarkdown(modelLabel)}` : "";
40229
- const header = `**${desc}** _\u00b7 ${formatFeedElapsed(r.elapsedMs)} \u00b7 ${r.toolCount} ${toolWord}${modelPart}_`;
40230
- const stepClean = stripMarkdown(r.currentStep).replace(/\s+/g, " ").trim();
40231
- const step = stepClean.length > 0 ? `\u2192 _${escapeMarkdown(truncate(stepClean, STATUS_LINE_MAX))}_` : `\u2192 _starting\u2026_`;
40232
- return [header, step];
40317
+ return `**${desc}** _\u00b7 ${formatFeedElapsed(r.elapsedMs)} \u00b7 ${r.toolCount} ${toolWord}${modelPart}_`;
40318
+ };
40319
+ const rowHistory = (r) => {
40320
+ const src = r.historyLines != null && r.historyLines.length > 0 ? r.historyLines : [r.currentStep];
40321
+ return src.filter((s) => s != null && stripMarkdown(s).replace(/\s+/g, " ").trim().length > 0);
40233
40322
  };
40234
40323
  const compose = (visibleCount) => {
40235
40324
  const shown = rows.slice(0, visibleCount);
40236
40325
  const hidden = rows.length - shown.length;
40326
+ const depth = combinedHistoryDepth(shown.length);
40237
40327
  const out = [`\uD83D\uDEE0 **Workers** \u00b7 _${rows.length} running_`];
40238
40328
  for (const r of shown) {
40239
- const [h, s] = rowLines(r);
40240
- out.push(h, s);
40329
+ out.push(rowHeader(r));
40330
+ const hist = rowHistory(r);
40331
+ if (hist.length === 0) {
40332
+ out.push("\u2192 _starting\u2026_");
40333
+ continue;
40334
+ }
40335
+ const esc = hist.slice(-depth).map(escapeStepLine);
40336
+ renderStepFeed(out, esc, false);
40241
40337
  }
40242
40338
  if (hidden > 0)
40243
40339
  out.push(`_+${hidden} more working\u2026_`);
@@ -40314,7 +40410,7 @@ function isWorkerActivityFeedEnabled(envVal) {
40314
40410
  var DESC_MAX = 80;
40315
40411
  function renderWorkerActivity(v, liveSuffix = "") {
40316
40412
  const desc = truncate(stripMarkdown(v.description).trim() || "background task", DESC_MAX);
40317
- const finished = v.state === "done" || v.state === "failed";
40413
+ const finished = v.state === "done" || v.state === "failed" || v.state === "incomplete";
40318
40414
  const rawSteps = (v.narrativeLines ?? []).filter((s) => s != null && s.trim().length > 0);
40319
40415
  let steps = rawSteps;
40320
40416
  if (steps.length === 0 && !finished) {
@@ -40332,7 +40428,7 @@ function renderWorkerActivity(v, liveSuffix = "") {
40332
40428
  model: v.model
40333
40429
  };
40334
40430
  let result;
40335
- if (finished) {
40431
+ if (finished && v.state !== "incomplete") {
40336
40432
  const text = cleanWorkerResultParagraph(v.latestSummary);
40337
40433
  if (text.length > 0)
40338
40434
  result = { emoji: v.state === "done" ? "\u2705" : "\u26a0\ufe0f", text };
@@ -40386,6 +40482,7 @@ function createWorkerActivityFeed(opts) {
40386
40482
  const firstPaintMin = opts.firstPaintMinMs ?? 8000;
40387
40483
  const heartbeatTickMs = opts.heartbeatTickMs ?? 6000;
40388
40484
  const maxRows = Math.max(1, Math.floor(opts.maxRows ?? 8));
40485
+ const staleWorkerTtlMs = Math.max(1, Math.floor(opts.staleWorkerTtlMs ?? 50 * 60000));
40389
40486
  const reconcilePinFn = opts.reconcilePin ?? (() => {});
40390
40487
  const setIntervalFn = opts.setInterval ?? ((cb, ms) => {
40391
40488
  const t = setInterval(cb, ms);
@@ -40487,6 +40584,7 @@ function createWorkerActivityFeed(opts) {
40487
40584
  elapsedMs: elapsedFor(r),
40488
40585
  toolCount: v.toolCount,
40489
40586
  currentStep,
40587
+ historyLines: r.narrative.length > 0 ? [...r.narrative] : undefined,
40490
40588
  model: v.model
40491
40589
  };
40492
40590
  });
@@ -40606,8 +40704,60 @@ function createWorkerActivityFeed(opts) {
40606
40704
  log(`worker-feed: edit transient error feed=${g.feedKey}: ${err.message}`);
40607
40705
  }
40608
40706
  }
40707
+ function finalizeWorker(group, agentId, row, view) {
40708
+ row.finished = true;
40709
+ row.state = view.state;
40710
+ markFinalized(agentId);
40711
+ group.chain = group.chain.then(() => {
40712
+ const others = runningRows(group).filter((w) => w.agentId !== agentId);
40713
+ if (others.length > 0) {
40714
+ removeWorker(group, agentId);
40715
+ syncPin(group);
40716
+ return doRender(group, { force: true });
40717
+ }
40718
+ const recap = { ...view, narrativeLines: [...row.narrative] };
40719
+ return doRender(group, { force: true, terminalRecap: recap, finishingAgentId: agentId });
40720
+ }).catch((err) => {
40721
+ log(`worker-feed: finalize chain error ${agentId}: ${err.message}`);
40722
+ });
40723
+ return group.chain;
40724
+ }
40725
+ function terminateWorker(agentId) {
40726
+ const g = groupOfAgent(agentId);
40727
+ const row = g?.workers.get(agentId);
40728
+ if (g == null || row == null) {
40729
+ markFinalized(agentId);
40730
+ return Promise.resolve();
40731
+ }
40732
+ if (row.finished) {
40733
+ return g.chain;
40734
+ }
40735
+ const lv = row.lastView;
40736
+ const view = {
40737
+ description: lv?.description ?? "background task",
40738
+ lastTool: null,
40739
+ toolCount: lv?.toolCount ?? 0,
40740
+ latestSummary: "",
40741
+ elapsedMs: liveElapsed(row, nowFn()),
40742
+ state: "incomplete",
40743
+ model: lv?.model
40744
+ };
40745
+ return finalizeWorker(g, agentId, row, view);
40746
+ }
40609
40747
  function heartbeatTick() {
40610
40748
  const now = nowFn();
40749
+ const staleAgentIds = [];
40750
+ for (const g of groups.values()) {
40751
+ for (const row of g.workers.values()) {
40752
+ if (!row.finished && now - row.lastUpdateAt >= staleWorkerTtlMs) {
40753
+ staleAgentIds.push(row.agentId);
40754
+ }
40755
+ }
40756
+ }
40757
+ for (const agentId of staleAgentIds) {
40758
+ log(`worker-feed: TTL reap agent=${agentId} \u2014 no update in ${Math.floor((now - (groupOfAgent(agentId)?.workers.get(agentId)?.lastUpdateAt ?? now)) / 1000)}s (>= ${Math.floor(staleWorkerTtlMs / 1000)}s); force-terminating leaked row`);
40759
+ terminateWorker(agentId);
40760
+ }
40611
40761
  for (const g of [...groups.values()]) {
40612
40762
  if (g.pendingFinalize != null && now >= g.cooldownUntil) {
40613
40763
  const recap = g.pendingFinalize;
@@ -40693,12 +40843,14 @@ function createWorkerActivityFeed(opts) {
40693
40843
  lastView: null,
40694
40844
  state: "running",
40695
40845
  finished: false,
40846
+ lastUpdateAt: nowFn(),
40696
40847
  dispatchAtMs: null,
40697
40848
  stepStartedAtMs: null
40698
40849
  };
40699
40850
  g.workers.set(agentId, row);
40700
40851
  agentIndex.set(agentId, feedKey);
40701
40852
  }
40853
+ row.lastUpdateAt = nowFn();
40702
40854
  accumulateNarrative(row, view);
40703
40855
  row.state = "running";
40704
40856
  row.lastView = { ...view, narrativeLines: [...row.narrative] };
@@ -40717,23 +40869,10 @@ function createWorkerActivityFeed(opts) {
40717
40869
  markFinalized(agentId);
40718
40870
  return Promise.resolve();
40719
40871
  }
40720
- row.finished = true;
40721
- row.state = view.state === "failed" ? "failed" : "done";
40722
- markFinalized(agentId);
40723
- const group = g;
40724
- group.chain = group.chain.then(() => {
40725
- const others = runningRows(group).filter((w) => w.agentId !== agentId);
40726
- if (others.length > 0) {
40727
- removeWorker(group, agentId);
40728
- syncPin(group);
40729
- return doRender(group, { force: true });
40730
- }
40731
- const recap = { ...view, narrativeLines: [...row.narrative] };
40732
- return doRender(group, { force: true, terminalRecap: recap, finishingAgentId: agentId });
40733
- }).catch((err) => {
40734
- log(`worker-feed: finish chain error ${agentId}: ${err.message}`);
40735
- });
40736
- return group.chain;
40872
+ return finalizeWorker(g, agentId, row, view);
40873
+ },
40874
+ terminate(agentId) {
40875
+ return terminateWorker(agentId);
40737
40876
  },
40738
40877
  drop(agentId) {
40739
40878
  markFinalized(agentId);
@@ -45037,6 +45176,9 @@ function runSilentTurnHeartbeatTick(view, deps) {
45037
45176
 
45038
45177
  // narrative-dedup.ts
45039
45178
  var REPLY_TOOLS = new Set(["reply", "stream_reply"]);
45179
+
45180
+ // narrative-dedup.ts
45181
+ var REPLY_TOOLS2 = new Set(["reply", "stream_reply"]);
45040
45182
  function normalizeNarrative(s) {
45041
45183
  return s.replace(/[*_`>#~]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
45042
45184
  }
@@ -45056,6 +45198,83 @@ function isDraftOfReply(textBlock, replyText) {
45056
45198
  return prefixSimilarity(textBlock, replyText) >= DRAFT_SUPPRESS_THRESHOLD;
45057
45199
  }
45058
45200
 
45201
+ // narrative-flush.ts
45202
+ var PENDING_NARRATIVE_FLUSH_MS = 250;
45203
+
45204
+ class NarrativeFlushController {
45205
+ effects;
45206
+ scheduler;
45207
+ flushMs;
45208
+ pending = null;
45209
+ timerShown = null;
45210
+ constructor(effects, scheduler, flushMs) {
45211
+ this.effects = effects;
45212
+ this.scheduler = scheduler;
45213
+ this.flushMs = flushMs;
45214
+ }
45215
+ get pendingText() {
45216
+ return this.pending;
45217
+ }
45218
+ get timerShownText() {
45219
+ return this.timerShown;
45220
+ }
45221
+ stage(text) {
45222
+ this.scheduler.disarm();
45223
+ if (this.pending != null) {
45224
+ this.effects.show(this.pending);
45225
+ }
45226
+ this.pending = text;
45227
+ this.scheduler.arm(() => this.onTimerFire(), this.flushMs);
45228
+ }
45229
+ onTimerFire() {
45230
+ if (this.pending == null)
45231
+ return;
45232
+ const text = this.pending;
45233
+ this.pending = null;
45234
+ this.timerShown = text;
45235
+ this.effects.show(text);
45236
+ }
45237
+ resolveOnTool(toolName, input) {
45238
+ this.scheduler.disarm();
45239
+ const replyText = REPLY_TOOLS2.has(toolName) && typeof input?.text === "string" ? input.text : null;
45240
+ if (replyText != null)
45241
+ this.maybeRetract(replyText);
45242
+ const pending = this.pending;
45243
+ if (pending == null)
45244
+ return;
45245
+ this.pending = null;
45246
+ if (replyText != null && isDraftOfReply(pending, replyText))
45247
+ return;
45248
+ this.effects.show(pending);
45249
+ }
45250
+ flushAtTurnEnd(lastReplyText) {
45251
+ this.scheduler.disarm();
45252
+ if (lastReplyText.length > 0)
45253
+ this.maybeRetract(lastReplyText);
45254
+ const pending = this.pending;
45255
+ if (pending == null)
45256
+ return;
45257
+ this.pending = null;
45258
+ if (lastReplyText.length > 0 && isDraftOfReply(pending, lastReplyText))
45259
+ return;
45260
+ this.effects.show(pending);
45261
+ }
45262
+ teardown() {
45263
+ this.scheduler.disarm();
45264
+ this.pending = null;
45265
+ this.timerShown = null;
45266
+ }
45267
+ maybeRetract(replyText) {
45268
+ const shown = this.timerShown;
45269
+ if (shown == null)
45270
+ return;
45271
+ if (!isDraftOfReply(shown, replyText))
45272
+ return;
45273
+ this.timerShown = null;
45274
+ this.effects.retractShown(shown);
45275
+ }
45276
+ }
45277
+
45059
45278
  // tool-labels.ts
45060
45279
  var MAX_LABEL_CHARS = 60;
45061
45280
  var MAX_BASH_CHARS = 40;
@@ -61788,6 +62007,7 @@ function startTimer(deps) {
61788
62007
  var EDIT_INTERVAL_MS = 60000;
61789
62008
  var POLL_INTERVAL_MS = 5000;
61790
62009
  var MAX_LIFETIME_MS = 30 * 60000;
62010
+ var BACKGROUND_WORK_SUPPRESS_TTL_MS = MAX_LIFETIME_MS;
61791
62011
  var TELEGRAM_MSG_CAP2 = 32768;
61792
62012
  var SUFFIX_RE = /\n\n(?:\u2014 |_)still working \(\d+m\)( \u00b7 message me anytime, I'll keep you posted)?_?$/;
61793
62013
  var stateByKey = new Map;
@@ -61805,6 +62025,7 @@ function ensure(key) {
61805
62025
  if (!s) {
61806
62026
  s = {
61807
62027
  pending: false,
62028
+ dispatchedAt: null,
61808
62029
  anchorMessageId: null,
61809
62030
  anchorOriginalText: "",
61810
62031
  anchorLiteralText: false,
@@ -61818,7 +62039,9 @@ function ensure(key) {
61818
62039
  function noteAsyncDispatch(key) {
61819
62040
  if (!enabled4())
61820
62041
  return;
61821
- ensure(key).pending = true;
62042
+ const s = ensure(key);
62043
+ s.pending = true;
62044
+ s.dispatchedAt = nowMs();
61822
62045
  }
61823
62046
  function noteOutbound3(key, opts) {
61824
62047
  if (!enabled4())
@@ -61848,6 +62071,17 @@ function noteTurnEnd(key) {
61848
62071
  function hasPendingAsyncDispatch(key) {
61849
62072
  return stateByKey.get(key)?.pending === true;
61850
62073
  }
62074
+ function anyPendingAsyncDispatchWithin(ttlMs) {
62075
+ if (ttlMs <= 0)
62076
+ return false;
62077
+ const now = nowMs();
62078
+ for (const s of stateByKey.values()) {
62079
+ if (s.pending === true && s.dispatchedAt != null && now - s.dispatchedAt < ttlMs) {
62080
+ return true;
62081
+ }
62082
+ }
62083
+ return false;
62084
+ }
61851
62085
  function clearPending(key, reason) {
61852
62086
  if (!stateByKey.has(key))
61853
62087
  return;
@@ -62015,6 +62249,30 @@ function clearSilentEndState(turnKey, deps) {
62015
62249
  `);
62016
62250
  } catch {}
62017
62251
  }
62252
+ var CAPTURED_PROSE_MIN_CHARS = 200;
62253
+ function decideCapturedProseDelivery(args, deps) {
62254
+ const minChars = args.minChars ?? CAPTURED_PROSE_MIN_CHARS;
62255
+ const state3 = readSilentEndState(deps);
62256
+ if (state3 == null)
62257
+ return { deliver: false, reason: "no-state" };
62258
+ if (state3.turnKey !== args.turnKey)
62259
+ return { deliver: false, reason: "turnkey-mismatch" };
62260
+ if (typeof state3.turnId === "string" && state3.turnId !== "" && args.turnId != null && args.turnId !== "" && state3.turnId !== args.turnId) {
62261
+ return { deliver: false, reason: "turnid-mismatch" };
62262
+ }
62263
+ const text4 = typeof state3.pendingText === "string" ? state3.pendingText : "";
62264
+ if (text4.trim().length < minChars)
62265
+ return { deliver: false, reason: "no-substantive-prose" };
62266
+ return { deliver: true, text: text4, reason: "captured-prose" };
62267
+ }
62268
+ function settleCapturedProseDelivery(outcome, effects) {
62269
+ if (outcome === "failed") {
62270
+ return { exhausted: effects.recordUndelivered().exhausted };
62271
+ }
62272
+ effects.closeObligation();
62273
+ effects.clearState();
62274
+ return { exhausted: false };
62275
+ }
62018
62276
  function readSilentEndState(deps) {
62019
62277
  const statePath = resolveStatePath2(deps);
62020
62278
  if (!existsSync14(statePath))
@@ -62120,6 +62378,56 @@ function isSilentFlushMarker(text4) {
62120
62378
  return SILENT_MARKERS.has(trimmed.toUpperCase());
62121
62379
  }
62122
62380
  var TRIVIAL_CONFIRMATIONS = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
62381
+ function isTrivialConfirmationLine(line) {
62382
+ let t = line.trim();
62383
+ if (t.length === 0 || t.length > 8)
62384
+ return false;
62385
+ if (/\W$/.test(t))
62386
+ t = t.slice(0, -1);
62387
+ return TRIVIAL_CONFIRMATIONS.has(t.toUpperCase());
62388
+ }
62389
+ function isCompositeSilentNoise(text4) {
62390
+ if (typeof text4 !== "string")
62391
+ return false;
62392
+ const lines = text4.split(`
62393
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
62394
+ if (lines.length === 0)
62395
+ return false;
62396
+ const hasMarker = lines.some((l) => isSilentFlushMarker(l));
62397
+ if (!hasMarker)
62398
+ return false;
62399
+ return lines.every((l) => isSilentFlushMarker(l) || isTrivialConfirmationLine(l));
62400
+ }
62401
+ function endsWithSilentMarker(text4) {
62402
+ if (typeof text4 !== "string")
62403
+ return false;
62404
+ const lines = text4.split(`
62405
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
62406
+ if (lines.length === 0)
62407
+ return false;
62408
+ return isSilentFlushMarker(lines[lines.length - 1]);
62409
+ }
62410
+ function decideTurnFlush(input) {
62411
+ const flushEnabled = input.flushEnabled !== false;
62412
+ if (!flushEnabled)
62413
+ return { kind: "skip", reason: "flag-disabled" };
62414
+ if (input.replyCalled)
62415
+ return { kind: "skip", reason: "reply-called" };
62416
+ if (input.chatId == null)
62417
+ return { kind: "skip", reason: "no-inbound-chat" };
62418
+ const joined = input.capturedText.join(`
62419
+
62420
+ `).trim();
62421
+ if (joined.length === 0)
62422
+ return { kind: "skip", reason: "empty-text" };
62423
+ if (isSilentFlushMarker(joined))
62424
+ return { kind: "skip", reason: "silent-marker" };
62425
+ if (isCompositeSilentNoise(joined))
62426
+ return { kind: "skip", reason: "silent-marker" };
62427
+ if (endsWithSilentMarker(joined))
62428
+ return { kind: "skip", reason: "silent-marker" };
62429
+ return { kind: "flush", text: joined };
62430
+ }
62123
62431
 
62124
62432
  // answer-stream.ts
62125
62433
  var MIN_INITIAL_CHARS = 50;
@@ -66523,6 +66831,86 @@ function hardenCardBreaks2(text4) {
66523
66831
  }
66524
66832
  return restore2(pieces.join(""));
66525
66833
  }
66834
+ var PARAGRAPH_SPACER2 = "\u00a0";
66835
+ function addParagraphSpacers2(text4) {
66836
+ if (!text4.includes(`
66837
+
66838
+ `))
66839
+ return text4;
66840
+ const nonce = Math.random().toString(36).slice(2);
66841
+ const { masked, restore: restore2, placeholder } = maskCodeRegions2(text4, nonce);
66842
+ if (!masked.includes(`
66843
+
66844
+ `))
66845
+ return restore2(masked);
66846
+ const SP = PARAGRAPH_SPACER2;
66847
+ const asciiTrim = (line) => line.replace(/^[ \t\r\f\v]+|[ \t\r\f\v]+$/g, "");
66848
+ const isBlankish = (line) => {
66849
+ const t = asciiTrim(line);
66850
+ return t === "" || t === SP;
66851
+ };
66852
+ const blockKind = (line) => {
66853
+ if (isFenceOpenLine2(line, placeholder))
66854
+ return "fence";
66855
+ if (isListItemLine2(line))
66856
+ return "list";
66857
+ if (isTableRowLine2(line) || isTableDelimiterLine2(line))
66858
+ return "table";
66859
+ if (isBlockquoteLine2(line))
66860
+ return "quote";
66861
+ if (isHeadingLine2(line))
66862
+ return "heading";
66863
+ if (/^(-{3,}|\*{3,}|_{3,})\s*$/.test(line.trimStart()))
66864
+ return "divider";
66865
+ return "prose";
66866
+ };
66867
+ const SAME_KIND_TIGHT = new Set([
66868
+ "list",
66869
+ "table",
66870
+ "quote",
66871
+ "fence",
66872
+ "divider"
66873
+ ]);
66874
+ const shouldSpaceGap = (above, below) => {
66875
+ const a = blockKind(above);
66876
+ const b = blockKind(below);
66877
+ if (a === b && SAME_KIND_TIGHT.has(a))
66878
+ return false;
66879
+ return true;
66880
+ };
66881
+ const toks = [];
66882
+ for (const line of masked.split(`
66883
+ `)) {
66884
+ const kind = isBlankish(line) ? "gap" : "content";
66885
+ const last = toks[toks.length - 1];
66886
+ if (last && last.kind === kind)
66887
+ last.lines.push(line);
66888
+ else
66889
+ toks.push({ kind, lines: [line] });
66890
+ }
66891
+ const out = [];
66892
+ for (let i = 0;i < toks.length; i++) {
66893
+ const tok = toks[i];
66894
+ if (tok.kind === "content") {
66895
+ out.push(...tok.lines);
66896
+ continue;
66897
+ }
66898
+ const prev = toks[i - 1];
66899
+ const next = toks[i + 1];
66900
+ if (prev?.kind === "content" && next?.kind === "content") {
66901
+ const above = prev.lines[prev.lines.length - 1];
66902
+ const below = next.lines[0];
66903
+ if (shouldSpaceGap(above, below))
66904
+ out.push("", SP, "");
66905
+ else
66906
+ out.push("");
66907
+ } else {
66908
+ out.push(...tok.lines);
66909
+ }
66910
+ }
66911
+ return restore2(out.join(`
66912
+ `));
66913
+ }
66526
66914
  function normalizePunctuation2(text4) {
66527
66915
  if (!/[\u2014\u2013\u2022\u00b7]/.test(text4))
66528
66916
  return text4;
@@ -66750,10 +67138,11 @@ function splitMarkdownChunks2(text4, maxLen = RICH_MESSAGE_MAX_CHARS2) {
66750
67138
  return chunks.map((c) => stripBoundarySpacers2(c, "trailing"));
66751
67139
  }
66752
67140
  function stripBoundarySpacers2(chunk2, side) {
67141
+ const sp = PARAGRAPH_SPACER2;
66753
67142
  if (side === "leading") {
66754
- return chunk2.replace(/^(?:[ \t]*\n)+/, "");
67143
+ return chunk2.replace(new RegExp(`^(?:[ \\t]*${sp}?[ \\t]*\\n+)+`), "");
66755
67144
  }
66756
- return chunk2.replace(/(?:\n[ \t]*)+$/, "");
67145
+ return chunk2.replace(new RegExp(`(?:\\n+[ \\t]*${sp}?[ \\t]*)+$`), "");
66757
67146
  }
66758
67147
  function backOffOpenFence2(text4, cut) {
66759
67148
  if (cut <= 0 || cut >= text4.length)
@@ -66943,8 +67332,8 @@ function normalizeOutboundBody(rawText, site, redact2) {
66943
67332
  }
66944
67333
  return { text: text4, voiceReplaced };
66945
67334
  }
66946
- function computeEffectiveText(text4, _literalText) {
66947
- return text4;
67335
+ function computeEffectiveText(text4, literalText) {
67336
+ return literalText ? text4 : addParagraphSpacers(text4);
66948
67337
  }
66949
67338
  function computeReplyChunks(args) {
66950
67339
  const { effectiveText, literalText, limit, chunkMode } = args;
@@ -67612,7 +68001,7 @@ function isSilentFlushMarker2(text4) {
67612
68001
  return SILENT_MARKERS2.has(trimmed.toUpperCase());
67613
68002
  }
67614
68003
  var TRIVIAL_CONFIRMATIONS2 = new Set(["SENT", "DONE", "OK", "OKAY", "ACK"]);
67615
- function isTrivialConfirmationLine(line) {
68004
+ function isTrivialConfirmationLine2(line) {
67616
68005
  let t = line.trim();
67617
68006
  if (t.length === 0 || t.length > 8)
67618
68007
  return false;
@@ -67620,7 +68009,7 @@ function isTrivialConfirmationLine(line) {
67620
68009
  t = t.slice(0, -1);
67621
68010
  return TRIVIAL_CONFIRMATIONS2.has(t.toUpperCase());
67622
68011
  }
67623
- function isCompositeSilentNoise(text4) {
68012
+ function isCompositeSilentNoise2(text4) {
67624
68013
  if (typeof text4 !== "string")
67625
68014
  return false;
67626
68015
  const lines = text4.split(`
@@ -67630,9 +68019,9 @@ function isCompositeSilentNoise(text4) {
67630
68019
  const hasMarker = lines.some((l) => isSilentFlushMarker2(l));
67631
68020
  if (!hasMarker)
67632
68021
  return false;
67633
- return lines.every((l) => isSilentFlushMarker2(l) || isTrivialConfirmationLine(l));
68022
+ return lines.every((l) => isSilentFlushMarker2(l) || isTrivialConfirmationLine2(l));
67634
68023
  }
67635
- function endsWithSilentMarker(text4) {
68024
+ function endsWithSilentMarker2(text4) {
67636
68025
  if (typeof text4 !== "string")
67637
68026
  return false;
67638
68027
  const lines = text4.split(`
@@ -67641,7 +68030,7 @@ function endsWithSilentMarker(text4) {
67641
68030
  return false;
67642
68031
  return isSilentFlushMarker2(lines[lines.length - 1]);
67643
68032
  }
67644
- function decideTurnFlush(input) {
68033
+ function decideTurnFlush2(input) {
67645
68034
  const flushEnabled = input.flushEnabled !== false;
67646
68035
  if (!flushEnabled)
67647
68036
  return { kind: "skip", reason: "flag-disabled" };
@@ -67656,9 +68045,9 @@ function decideTurnFlush(input) {
67656
68045
  return { kind: "skip", reason: "empty-text" };
67657
68046
  if (isSilentFlushMarker2(joined))
67658
68047
  return { kind: "skip", reason: "silent-marker" };
67659
- if (isCompositeSilentNoise(joined))
68048
+ if (isCompositeSilentNoise2(joined))
67660
68049
  return { kind: "skip", reason: "silent-marker" };
67661
- if (endsWithSilentMarker(joined))
68050
+ if (endsWithSilentMarker2(joined))
67662
68051
  return { kind: "skip", reason: "silent-marker" };
67663
68052
  return { kind: "flush", text: joined };
67664
68053
  }
@@ -67672,6 +68061,72 @@ function isTurnFlushSafetyEnabled(env = process.env) {
67672
68061
  return true;
67673
68062
  }
67674
68063
 
68064
+ // answer-ready-flush.ts
68065
+ var ANSWER_READY_FLUSH_MS = 1000;
68066
+ function resolveAnswerReadyFlushMs(env) {
68067
+ const raw = env.SWITCHROOM_ANSWER_READY_FLUSH_MS;
68068
+ if (raw == null || raw.trim() === "")
68069
+ return ANSWER_READY_FLUSH_MS;
68070
+ const n = Number(raw);
68071
+ if (!Number.isFinite(n))
68072
+ return ANSWER_READY_FLUSH_MS;
68073
+ if (n <= 0)
68074
+ return 0;
68075
+ return Math.floor(n);
68076
+ }
68077
+ function shouldArmAnswerReadyFlush(input) {
68078
+ if (input.flushWindowMs <= 0)
68079
+ return false;
68080
+ if (input.inFlightToolCount > 0)
68081
+ return false;
68082
+ if (input.hasPendingAsyncDispatch)
68083
+ return false;
68084
+ return decideTurnFlush(input.flush).kind === "flush";
68085
+ }
68086
+
68087
+ class AnswerReadyFlushController {
68088
+ deps;
68089
+ constructor(deps) {
68090
+ this.deps = deps;
68091
+ }
68092
+ get setTimeoutFn() {
68093
+ return this.deps.setTimeoutFn ?? ((fn, ms) => setTimeout(fn, ms));
68094
+ }
68095
+ get clearTimeoutFn() {
68096
+ return this.deps.clearTimeoutFn ?? ((h) => clearTimeout(h));
68097
+ }
68098
+ clear(turn) {
68099
+ if (turn == null)
68100
+ return;
68101
+ const handle = this.deps.getTimerHandle(turn);
68102
+ if (handle != null) {
68103
+ this.clearTimeoutFn(handle);
68104
+ this.deps.setTimerHandle(turn, null);
68105
+ }
68106
+ }
68107
+ reset() {
68108
+ const turn = this.deps.getCurrentTurn();
68109
+ this.clear(turn);
68110
+ if (turn == null)
68111
+ return;
68112
+ const armInput = this.deps.getArmInput(turn);
68113
+ if (!shouldArmAnswerReadyFlush(armInput))
68114
+ return;
68115
+ const handle = this.setTimeoutFn(() => this.onExpiry(turn), armInput.flushWindowMs);
68116
+ this.deps.setTimerHandle(turn, handle);
68117
+ }
68118
+ onExpiry(armedTurn) {
68119
+ const live = this.deps.getCurrentTurn();
68120
+ if (live == null || live !== armedTurn)
68121
+ return;
68122
+ this.deps.setTimerHandle(live, null);
68123
+ if (!shouldArmAnswerReadyFlush(this.deps.getArmInput(live)))
68124
+ return;
68125
+ this.deps.log?.("answer-ready quiescence flush \u2014 delivering composed terminal answer");
68126
+ this.deps.onFlush(live);
68127
+ }
68128
+ }
68129
+
67675
68130
  // gateway/turn-end-gate.ts
67676
68131
  function decideTurnEndGate(snapshot) {
67677
68132
  const { flushDecision, finalAnswerDelivered } = snapshot;
@@ -69098,11 +69553,12 @@ async function injectSlashCommand(agentName3, command, opts = {}) {
69098
69553
  session,
69099
69554
  command: command.trim(),
69100
69555
  settleMs,
69101
- timeoutMs
69556
+ timeoutMs,
69557
+ precondition: opts.precondition
69102
69558
  }));
69103
69559
  }
69104
69560
  async function injectSlashCommandWith(runner, args) {
69105
- const { socket, session, command, settleMs, timeoutMs } = args;
69561
+ const { socket, session, command, settleMs, timeoutMs, precondition } = args;
69106
69562
  let bareVerb;
69107
69563
  try {
69108
69564
  bareVerb = validateInjectCommand(command);
@@ -69132,6 +69588,17 @@ async function injectSlashCommandWith(runner, args) {
69132
69588
  errorMessage: `tmux session "${session}" on socket "${socket}" not found. ` + `Is the agent running under the tmux supervisor (the default)? ` + `If experimental.legacy_pty=true is set, inject is unsupported.`
69133
69589
  };
69134
69590
  }
69591
+ if (precondition && !precondition()) {
69592
+ return {
69593
+ outcome: "skipped",
69594
+ output: "",
69595
+ truncated: false,
69596
+ command: bareVerb,
69597
+ meta,
69598
+ errorCode: "precondition_failed",
69599
+ errorMessage: "inject precondition returned false at write time; send aborted " + "(no keys sent)."
69600
+ };
69601
+ }
69135
69602
  const before = runner.capture(socket, session) ?? "";
69136
69603
  try {
69137
69604
  runner.send(socket, session, ["send-keys", "-l", command]);
@@ -69336,6 +69803,12 @@ _truncated_`;
69336
69803
  }
69337
69804
  return { body: verbHtml, accent: "done" };
69338
69805
  }
69806
+ if (result.outcome === "skipped") {
69807
+ return {
69808
+ body: `${verbHtml} \u2014 skipped (precondition not met at send time)`,
69809
+ accent: "issue"
69810
+ };
69811
+ }
69339
69812
  const code2 = result.errorCode ?? "tmux_failed";
69340
69813
  const msg = result.errorMessage ?? "unknown error";
69341
69814
  if (code2 === "blocked") {
@@ -72018,6 +72491,8 @@ function decideIdleClear(state3, now) {
72018
72491
  return { clear: false };
72019
72492
  if (state3.turnInFlight)
72020
72493
  return { clear: false };
72494
+ if (state3.backgroundWorkInFlight)
72495
+ return { clear: false };
72021
72496
  if (state3.alreadyCleared)
72022
72497
  return { clear: false };
72023
72498
  if (now - lastEventAt(state3) < state3.idleClearMs)
@@ -72029,6 +72504,67 @@ function classifyIdleEvent(kind, durationMs) {
72029
72504
  const synthetic = turnEnded && durationMs === -1;
72030
72505
  return { activity: !synthetic, turnEnded };
72031
72506
  }
72507
+
72508
+ class IdleTracker {
72509
+ lastActivityAt;
72510
+ lastTurnEndedAt = null;
72511
+ alreadyCleared = false;
72512
+ dispatching = false;
72513
+ constructor(startedAt) {
72514
+ this.lastActivityAt = startedAt;
72515
+ }
72516
+ get activityAt() {
72517
+ return this.lastActivityAt;
72518
+ }
72519
+ get turnEndedAt() {
72520
+ return this.lastTurnEndedAt;
72521
+ }
72522
+ get cleared() {
72523
+ return this.alreadyCleared;
72524
+ }
72525
+ get isDispatching() {
72526
+ return this.dispatching;
72527
+ }
72528
+ noteInbound(now) {
72529
+ this.lastActivityAt = now;
72530
+ this.alreadyCleared = false;
72531
+ }
72532
+ noteEvent(kind, now, durationMs) {
72533
+ const signal = classifyIdleEvent(kind, durationMs);
72534
+ if (signal.activity)
72535
+ this.noteInbound(now);
72536
+ if (signal.turnEnded)
72537
+ this.lastTurnEndedAt = now;
72538
+ }
72539
+ stateFor(inputs, alreadyCleared) {
72540
+ return {
72541
+ lastActivityAt: this.lastActivityAt,
72542
+ lastTurnEndedAt: this.lastTurnEndedAt,
72543
+ idleClearMs: inputs.idleClearMs,
72544
+ alreadyCleared,
72545
+ turnInFlight: inputs.turnInFlight,
72546
+ backgroundWorkInFlight: inputs.backgroundWorkInFlight
72547
+ };
72548
+ }
72549
+ decide(now, inputs) {
72550
+ return decideIdleClear(this.stateFor(inputs, this.alreadyCleared), now);
72551
+ }
72552
+ decideIgnoringLatch(now, inputs) {
72553
+ return decideIdleClear(this.stateFor(inputs, false), now);
72554
+ }
72555
+ markClearFired() {
72556
+ this.alreadyCleared = true;
72557
+ }
72558
+ reArm() {
72559
+ this.alreadyCleared = false;
72560
+ }
72561
+ beginDispatch() {
72562
+ this.dispatching = true;
72563
+ }
72564
+ endDispatch() {
72565
+ this.dispatching = false;
72566
+ }
72567
+ }
72032
72568
  function idleDurationToMs(raw) {
72033
72569
  const m = /^(\d+)([smh])$/.exec(raw.trim());
72034
72570
  if (!m)
@@ -74350,6 +74886,9 @@ function cronIdentity(agent) {
74350
74886
  function isCronIdentity(name) {
74351
74887
  return typeof name === "string" && name.endsWith(CRON_IDENTITY_SUFFIX);
74352
74888
  }
74889
+ function isCronInjectFire(meta) {
74890
+ return meta?.source === "cron" || meta?.session === "cron";
74891
+ }
74353
74892
  function resolveInjectTarget(agentName3, meta) {
74354
74893
  return meta?.session === "cron" ? cronIdentity(agentName3) : agentName3;
74355
74894
  }
@@ -75612,6 +76151,44 @@ function maybeRotate(path2, fs2, maxBytes = TURNS_JSONL_MAX_BYTES) {
75612
76151
  return true;
75613
76152
  }
75614
76153
 
76154
+ // gateway/turn-record-status.ts
76155
+ function computeTurnStatus(turn) {
76156
+ switch (turn.deliveryOutcome) {
76157
+ case "failed":
76158
+ return "send_failed";
76159
+ case "delivered":
76160
+ return "complete";
76161
+ case "suppressed":
76162
+ return turn.finalAnswerDelivered ? "complete" : "no_reply";
76163
+ default:
76164
+ return turn.finalAnswerDelivered ? "complete" : "no_reply";
76165
+ }
76166
+ }
76167
+ function backstopSendOutcome(args) {
76168
+ if (args.threw)
76169
+ return "failed";
76170
+ if (args.chunkCount === 0)
76171
+ return "failed";
76172
+ if (args.sentCount < args.chunkCount)
76173
+ return "failed";
76174
+ return "delivered";
76175
+ }
76176
+ function finalizeBackstopSend(turn, send) {
76177
+ const outcome = backstopSendOutcome(send);
76178
+ turn.deliveryOutcome = outcome;
76179
+ return outcome;
76180
+ }
76181
+ function buildTurnRecord(turn, endedAt) {
76182
+ return {
76183
+ ts: Math.floor(endedAt / 1000),
76184
+ agent: turn.agent,
76185
+ duration_ms: turn.startedAt > 0 ? endedAt - turn.startedAt : 0,
76186
+ tools: turn.toolCallCount ?? 0,
76187
+ status: computeTurnStatus(turn),
76188
+ turn_id: turn.turnId
76189
+ };
76190
+ }
76191
+
75615
76192
  // gateway/inbound-delivery-confirm.ts
75616
76193
  function createDeliveryQueue() {
75617
76194
  return { pending: new Map };
@@ -76682,6 +77259,9 @@ function decideSubagentProgress(input) {
76682
77259
  if (isEnvFlagOn(input.disableEnvValue)) {
76683
77260
  return { deliver: false, reason: "env-disabled" };
76684
77261
  }
77262
+ if (input.skeleton === true) {
77263
+ return { deliver: false, reason: "skeleton-liveness" };
77264
+ }
76685
77265
  if (!input.isBackground) {
76686
77266
  return { deliver: false, reason: "foreground" };
76687
77267
  }
@@ -78210,25 +78790,81 @@ function redactSecrets(text4) {
78210
78790
  return out;
78211
78791
  }
78212
78792
 
78213
- // narrative-dedup.ts
78214
- var REPLY_TOOLS2 = new Set(["reply", "stream_reply"]);
78215
- function normalizeNarrative2(s) {
78216
- return s.replace(/[*_`>#~]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
78217
- }
78218
- function prefixSimilarity2(a, b) {
78219
- const x = normalizeNarrative2(a);
78220
- const y = normalizeNarrative2(b);
78221
- if (x.length === 0 || y.length === 0)
78222
- return 0;
78223
- const n = Math.min(x.length, y.length);
78224
- let i = 0;
78225
- while (i < n && x[i] === y[i])
78226
- i++;
78227
- return i / n;
78228
- }
78229
- var DRAFT_SUPPRESS_THRESHOLD2 = 0.8;
78230
- function isDraftOfReply2(textBlock, replyText) {
78231
- return prefixSimilarity2(textBlock, replyText) >= DRAFT_SUPPRESS_THRESHOLD2;
78793
+ // narrative-flush.ts
78794
+ var PENDING_NARRATIVE_FLUSH_MS2 = 250;
78795
+
78796
+ class NarrativeFlushController2 {
78797
+ effects;
78798
+ scheduler;
78799
+ flushMs;
78800
+ pending = null;
78801
+ timerShown = null;
78802
+ constructor(effects, scheduler, flushMs) {
78803
+ this.effects = effects;
78804
+ this.scheduler = scheduler;
78805
+ this.flushMs = flushMs;
78806
+ }
78807
+ get pendingText() {
78808
+ return this.pending;
78809
+ }
78810
+ get timerShownText() {
78811
+ return this.timerShown;
78812
+ }
78813
+ stage(text4) {
78814
+ this.scheduler.disarm();
78815
+ if (this.pending != null) {
78816
+ this.effects.show(this.pending);
78817
+ }
78818
+ this.pending = text4;
78819
+ this.scheduler.arm(() => this.onTimerFire(), this.flushMs);
78820
+ }
78821
+ onTimerFire() {
78822
+ if (this.pending == null)
78823
+ return;
78824
+ const text4 = this.pending;
78825
+ this.pending = null;
78826
+ this.timerShown = text4;
78827
+ this.effects.show(text4);
78828
+ }
78829
+ resolveOnTool(toolName, input) {
78830
+ this.scheduler.disarm();
78831
+ const replyText = REPLY_TOOLS2.has(toolName) && typeof input?.text === "string" ? input.text : null;
78832
+ if (replyText != null)
78833
+ this.maybeRetract(replyText);
78834
+ const pending = this.pending;
78835
+ if (pending == null)
78836
+ return;
78837
+ this.pending = null;
78838
+ if (replyText != null && isDraftOfReply(pending, replyText))
78839
+ return;
78840
+ this.effects.show(pending);
78841
+ }
78842
+ flushAtTurnEnd(lastReplyText) {
78843
+ this.scheduler.disarm();
78844
+ if (lastReplyText.length > 0)
78845
+ this.maybeRetract(lastReplyText);
78846
+ const pending = this.pending;
78847
+ if (pending == null)
78848
+ return;
78849
+ this.pending = null;
78850
+ if (lastReplyText.length > 0 && isDraftOfReply(pending, lastReplyText))
78851
+ return;
78852
+ this.effects.show(pending);
78853
+ }
78854
+ teardown() {
78855
+ this.scheduler.disarm();
78856
+ this.pending = null;
78857
+ this.timerShown = null;
78858
+ }
78859
+ maybeRetract(replyText) {
78860
+ const shown = this.timerShown;
78861
+ if (shown == null)
78862
+ return;
78863
+ if (!isDraftOfReply(shown, replyText))
78864
+ return;
78865
+ this.timerShown = null;
78866
+ this.effects.retractShown(shown);
78867
+ }
78232
78868
  }
78233
78869
 
78234
78870
  // subagent-watcher.ts
@@ -78365,6 +79001,9 @@ var DEFAULT_STALL_THRESHOLD_MS = 60000;
78365
79001
  var DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS = 300000;
78366
79002
  var DEFAULT_SILENT_STALL_TERMINAL_MS = 300000;
78367
79003
  var DEFAULT_INFLIGHT_TERMINAL_CAP_MS = 45 * 60000;
79004
+ function resolveInflightTerminalCapMs(configVal) {
79005
+ return configVal ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_TERMINAL_CAP_MS") ?? DEFAULT_INFLIGHT_TERMINAL_CAP_MS;
79006
+ }
78368
79007
  var DEFAULT_DEFERRAL_LOG_INTERVAL_MS = 60000;
78369
79008
  var LONG_RUNNING_TOOLS = new Set(["Bash"]);
78370
79009
  var MAX_RESURRECTIONS = 1;
@@ -78478,13 +79117,47 @@ function backfillJsonlAgentId(db2, jsonlPath, agentId, log) {
78478
79117
  }
78479
79118
  function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, parentStateDir, onUnstall, onFileVanished, onProgress) {
78480
79119
  try {
79120
+ if (entry.narrativeGate != null)
79121
+ entry.narrativeGate.tick(now);
78481
79122
  const stat = fs2.statSync(entry.filePath);
78482
79123
  if (stat.size < tail.cursor) {
78483
79124
  tail.cursor = 0;
78484
79125
  tail.pendingPartial = "";
78485
79126
  }
78486
- if (stat.size === tail.cursor)
79127
+ if (stat.size === tail.cursor) {
79128
+ if (onProgress != null && entry.state === "running" && !entry.historical) {
79129
+ let hasChild = false;
79130
+ if (db2 != null) {
79131
+ try {
79132
+ const kid = db2.prepare("SELECT 1 FROM subagents WHERE parent_agent_id = ? LIMIT 1").get(entry.agentId);
79133
+ hasChild = kid != null;
79134
+ } catch (kidErr) {
79135
+ log?.(`subagent-watcher: skeleton child-check error ${entry.agentId}: ${kidErr.message}`);
79136
+ }
79137
+ }
79138
+ if (!hasChild) {
79139
+ try {
79140
+ onProgress({
79141
+ agentId: entry.agentId,
79142
+ description: entry.description,
79143
+ latestSummary: "",
79144
+ elapsedMs: now - entry.dispatchedAt,
79145
+ prevBucketIdx: entry.lastProgressBucketIdx,
79146
+ setBucketIdx: (b) => {
79147
+ entry.lastProgressBucketIdx = b;
79148
+ },
79149
+ lastTool: entry.lastTool,
79150
+ toolCount: entry.toolCount,
79151
+ model: entry.currentModel,
79152
+ skeleton: true
79153
+ });
79154
+ } catch (cbErr) {
79155
+ log?.(`subagent-watcher: onProgress (skeleton) callback error ${entry.agentId}: ${cbErr.message}`);
79156
+ }
79157
+ }
79158
+ }
78487
79159
  return;
79160
+ }
78488
79161
  const buf = Buffer.alloc(stat.size - tail.cursor);
78489
79162
  const fd = fs2.openSync(entry.filePath, "r");
78490
79163
  try {
@@ -78538,43 +79211,87 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
78538
79211
  entry.errorDetail = errInfo.detail.slice(0, SUBAGENT_RESULT_TEXT_MAX);
78539
79212
  }
78540
79213
  const events = projectSubagentLine(line, entry.agentId, startState);
78541
- const fireNarrativeProgress = () => {
78542
- if (onProgress == null || entry.state !== "running" || entry.historical)
78543
- return false;
78544
- try {
78545
- onProgress({
78546
- agentId: entry.agentId,
78547
- description: entry.description,
78548
- latestSummary: entry.lastResultText,
78549
- elapsedMs: now - entry.dispatchedAt,
78550
- prevBucketIdx: entry.lastProgressBucketIdx,
78551
- setBucketIdx: (b) => {
78552
- entry.lastProgressBucketIdx = b;
78553
- },
78554
- lastTool: entry.lastTool,
78555
- toolCount: entry.toolCount,
78556
- model: entry.currentModel
78557
- });
78558
- return true;
78559
- } catch (cbErr) {
78560
- log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${cbErr.message}`);
78561
- return false;
78562
- }
79214
+ const buildNarrativeGate = () => {
79215
+ const nowRef = { value: now };
79216
+ let cueFired = false;
79217
+ const fireCue = () => {
79218
+ if (onProgress == null || entry.state !== "running" || entry.historical)
79219
+ return false;
79220
+ try {
79221
+ onProgress({
79222
+ agentId: entry.agentId,
79223
+ description: entry.description,
79224
+ latestSummary: entry.lastResultText,
79225
+ elapsedMs: nowRef.value - entry.dispatchedAt,
79226
+ prevBucketIdx: entry.lastProgressBucketIdx,
79227
+ setBucketIdx: (b) => {
79228
+ entry.lastProgressBucketIdx = b;
79229
+ },
79230
+ lastTool: entry.lastTool,
79231
+ toolCount: entry.toolCount,
79232
+ model: entry.currentModel
79233
+ });
79234
+ return true;
79235
+ } catch (cbErr) {
79236
+ log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${cbErr.message}`);
79237
+ return false;
79238
+ }
79239
+ };
79240
+ const scheduler = {
79241
+ armedFn: null,
79242
+ deadline: 0
79243
+ };
79244
+ const controller = new NarrativeFlushController2({
79245
+ show: () => {
79246
+ cueFired = fireCue();
79247
+ },
79248
+ retractShown: () => {
79249
+ log?.(`subagent-watcher: narrative early-paint retract (no-op on replace-on-write worker card) ${entry.agentId}`);
79250
+ }
79251
+ }, {
79252
+ arm: (fn, ms) => {
79253
+ scheduler.armedFn = fn;
79254
+ scheduler.deadline = nowRef.value + ms;
79255
+ },
79256
+ disarm: () => {
79257
+ scheduler.armedFn = null;
79258
+ }
79259
+ }, PENDING_NARRATIVE_FLUSH_MS2);
79260
+ return {
79261
+ tick: (n) => {
79262
+ nowRef.value = n;
79263
+ if (scheduler.armedFn != null && n >= scheduler.deadline) {
79264
+ const fn = scheduler.armedFn;
79265
+ scheduler.armedFn = null;
79266
+ fn();
79267
+ }
79268
+ },
79269
+ stage: (text5) => {
79270
+ controller.stage(text5);
79271
+ },
79272
+ resolveOnTool: (toolName, input) => {
79273
+ cueFired = false;
79274
+ controller.resolveOnTool(toolName ?? "", input);
79275
+ return cueFired;
79276
+ },
79277
+ resolveAtTurnEnd: (lastReplyText) => {
79278
+ cueFired = false;
79279
+ controller.flushAtTurnEnd(lastReplyText);
79280
+ return cueFired;
79281
+ },
79282
+ reset: () => {
79283
+ controller.teardown();
79284
+ scheduler.armedFn = null;
79285
+ }
79286
+ };
78563
79287
  };
78564
79288
  const resolvePendingSubNarrative = (toolName, toolInput) => {
78565
- if (entry.pendingNarrative == null)
79289
+ if (entry.narrativeGate == null)
78566
79290
  return false;
78567
- const pending = entry.pendingNarrative;
78568
- entry.pendingNarrative = null;
78569
- if (toolName != null && REPLY_TOOLS2.has(toolName)) {
78570
- const replyText = typeof toolInput?.text === "string" ? toolInput.text : "";
78571
- if (isDraftOfReply2(pending.text, replyText))
78572
- return false;
78573
- } else if (toolName == null && entry.lastReplyText != null && entry.lastReplyText.length > 0) {
78574
- if (isDraftOfReply2(pending.text, entry.lastReplyText))
78575
- return false;
79291
+ if (toolName == null) {
79292
+ return entry.narrativeGate.resolveAtTurnEnd(entry.lastReplyText ?? "");
78576
79293
  }
78577
- return fireNarrativeProgress();
79294
+ return entry.narrativeGate.resolveOnTool(toolName, toolInput);
78578
79295
  };
78579
79296
  for (const ev of events) {
78580
79297
  const idleSecBeforeBump = Math.round((now - entry.lastActivityAt) / 1000);
@@ -78681,10 +79398,9 @@ function readSubTail(entry, tail, now, onDescriptionUpdate, fs2, log, db2, paren
78681
79398
  } else if (ev.kind === "sub_agent_text") {
78682
79399
  entry.lastSummaryLine = clipNarrative(ev.text);
78683
79400
  entry.lastResultText = ev.text.trim().slice(0, SUBAGENT_RESULT_TEXT_MAX);
78684
- if (entry.pendingNarrative != null) {
78685
- fireNarrativeProgress();
78686
- }
78687
- entry.pendingNarrative = { text: ev.text };
79401
+ if (entry.narrativeGate == null)
79402
+ entry.narrativeGate = buildNarrativeGate();
79403
+ entry.narrativeGate.stage(ev.text);
78688
79404
  } else if (ev.kind === "sub_agent_tool_result") {
78689
79405
  if (ev.toolUseId != null && ev.toolUseId !== "") {
78690
79406
  entry.inflightToolUseIds.delete(ev.toolUseId);
@@ -78731,7 +79447,7 @@ function startSubagentWatcher(config) {
78731
79447
  const stallThresholdMs = config.stallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_MS") ?? DEFAULT_STALL_THRESHOLD_MS;
78732
79448
  const silentSynthesisStallThresholdMs = config.silentSynthesisStallThresholdMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_SILENT_SYNTH_STALL_MS") ?? DEFAULT_SILENT_SYNTHESIS_STALL_THRESHOLD_MS;
78733
79449
  const silentStallTerminalMs = config.silentStallTerminalMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_STALL_TERMINAL_MS") ?? DEFAULT_SILENT_STALL_TERMINAL_MS;
78734
- const inflightTerminalCapMs = config.inflightTerminalCapMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_TERMINAL_CAP_MS") ?? DEFAULT_INFLIGHT_TERMINAL_CAP_MS;
79450
+ const inflightTerminalCapMs = resolveInflightTerminalCapMs(config.inflightTerminalCapMs);
78735
79451
  const deferralLogIntervalMs = config.deferralLogIntervalMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_DEFERRAL_LOG_INTERVAL_MS") ?? DEFAULT_DEFERRAL_LOG_INTERVAL_MS;
78736
79452
  const inflightPromoteMaxAgeMs = config.inflightPromoteMaxAgeMs ?? parseEnvMs("SWITCHROOM_SUBAGENT_INFLIGHT_MAX_AGE_MS") ?? DEFAULT_INFLIGHT_PROMOTE_MAX_AGE_MS;
78737
79453
  const terminatedAgentIdsCap = config.terminatedAgentIdsCap ?? TERMINATED_AGENT_IDS_CAP;
@@ -78892,7 +79608,8 @@ function startSubagentWatcher(config) {
78892
79608
  }
78893
79609
  entry.toolCount = 0;
78894
79610
  entry.lastTool = null;
78895
- entry.pendingNarrative = null;
79611
+ entry.narrativeGate?.reset();
79612
+ entry.narrativeGate = null;
78896
79613
  tail.cursor = 0;
78897
79614
  tail.pendingPartial = "";
78898
79615
  tail.hasEmittedStart = false;
@@ -78991,6 +79708,13 @@ function startSubagentWatcher(config) {
78991
79708
  }
78992
79709
  terminatedAgentIds.add(agentId);
78993
79710
  log?.(`subagent-watcher: cleaned up terminal agent ${agentId}`);
79711
+ if (config.onTerminalCleanup) {
79712
+ try {
79713
+ config.onTerminalCleanup(agentId);
79714
+ } catch (cbErr) {
79715
+ log?.(`subagent-watcher: onTerminalCleanup callback error ${agentId}: ${cbErr.message}`);
79716
+ }
79717
+ }
78994
79718
  }
78995
79719
  function recordFalseFinish(agentId, filePath, n) {
78996
79720
  let size = 0;
@@ -82034,10 +82758,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
82034
82758
  }
82035
82759
 
82036
82760
  // ../src/build-info.ts
82037
- var VERSION = "0.18.19";
82038
- var COMMIT_SHA = "34c72776";
82039
- var COMMIT_DATE = "2026-07-13T14:06:25+10:00";
82040
- var LATEST_PR = 3212;
82761
+ var VERSION = "0.18.21";
82762
+ var COMMIT_SHA = "c237ff59";
82763
+ var COMMIT_DATE = "2026-07-14T01:11:18+10:00";
82764
+ var LATEST_PR = 3234;
82041
82765
  var COMMITS_AHEAD_OF_TAG = 0;
82042
82766
 
82043
82767
  // gateway/boot-version.ts
@@ -83725,6 +84449,7 @@ function applySubagentsSchema(db2) {
83725
84449
  db2.exec("ALTER TABLE subagents ADD COLUMN model TEXT");
83726
84450
  }
83727
84451
  db2.exec("CREATE INDEX IF NOT EXISTS subagents_jsonl_id ON subagents(jsonl_agent_id)");
84452
+ db2.exec("CREATE INDEX IF NOT EXISTS subagents_parent_agent ON subagents(parent_agent_id)");
83728
84453
  }
83729
84454
  function mapSubagentRow(row) {
83730
84455
  return {
@@ -84462,6 +85187,7 @@ function resolveSubagentOriginChat(agentId) {
84462
85187
  }
84463
85188
  }
84464
85189
  var WORKER_FEED_FALLBACK_LOG_CAP = 256;
85190
+ var WORKER_FEED_STALE_TTL_MARGIN_MS = 300000;
84465
85191
  var workerFeedOwnerDmFallbackLogged = new Set;
84466
85192
  function resolveWorkerFeedChat(agentId, fleetChatId) {
84467
85193
  const origin = resolveSubagentOriginChat(agentId);
@@ -85359,14 +86085,14 @@ function releaseTurnBufferGate(key, endingTurn) {
85359
86085
  }
85360
86086
  function emitTurnRecord(turn, endedAt) {
85361
86087
  try {
85362
- const rec = JSON.stringify({
85363
- ts: Math.floor(endedAt / 1000),
86088
+ const rec = JSON.stringify(buildTurnRecord({
85364
86089
  agent: process.env.SWITCHROOM_AGENT_NAME ?? "unknown",
85365
- duration_ms: turn.startedAt > 0 ? endedAt - turn.startedAt : 0,
85366
- tools: turn.toolCallCount ?? 0,
85367
- status: turn.finalAnswerDelivered ? "complete" : "no_reply",
85368
- turn_id: turn.turnId
85369
- }) + `
86090
+ startedAt: turn.startedAt,
86091
+ toolCallCount: turn.toolCallCount ?? 0,
86092
+ turnId: turn.turnId,
86093
+ finalAnswerDelivered: turn.finalAnswerDelivered,
86094
+ deliveryOutcome: turn.deliveryOutcome
86095
+ }, endedAt)) + `
85370
86096
  `;
85371
86097
  const turnsPath = "/state/agent/turns.jsonl";
85372
86098
  maybeRotate(turnsPath, {
@@ -85382,15 +86108,18 @@ function emitTurnRecord(turn, endedAt) {
85382
86108
  appendFileSync6(turnsPath, rec);
85383
86109
  } catch {}
85384
86110
  }
85385
- function endCurrentTurnAtomic(turn) {
86111
+ function endCurrentTurnAtomic(turn, opts) {
85386
86112
  const key = statusKey(turn.sessionChatId, turn.sessionThreadId);
85387
86113
  if (!turnLiveForItsTopic(turn))
85388
- return;
86114
+ return null;
86115
+ clearAnswerReadyFlushTimeout(turn);
85389
86116
  endCurrentTurnForKey(turn, key);
85390
86117
  const turnEndedAt = Date.now();
85391
86118
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("clear", "turn_end", turn, turnEndedAt)}
85392
86119
  `);
85393
- emitTurnRecord(turn, turnEndedAt);
86120
+ if (opts?.deferRecord !== true) {
86121
+ emitTurnRecord(turn, turnEndedAt);
86122
+ }
85394
86123
  const degraded = detectStatusSurfaceDegraded(turn);
85395
86124
  if (degraded != null) {
85396
86125
  process.stderr.write(`telegram gateway: status-surface DEGRADED reason=${degraded.reason} turnId=${turn.turnId} chat=${turn.sessionChatId} thread=${turn.sessionThreadId ?? "-"} ${degraded.detail}
@@ -85407,8 +86136,10 @@ function endCurrentTurnAtomic(turn) {
85407
86136
  clearTimeout(turn.noReplyDrainTimer);
85408
86137
  turn.noReplyDrainTimer = null;
85409
86138
  }
86139
+ turn.narrativeGate?.teardown();
85410
86140
  purgeReactionTracking(statusKey(turn.sessionChatId, turn.sessionThreadId), turn);
85411
86141
  armNoReplyDrainTimer(turn);
86142
+ return turnEndedAt;
85412
86143
  }
85413
86144
  function maybeProactiveCompact() {
85414
86145
  if (compactDispatching)
@@ -85468,16 +86199,9 @@ function maybeProactiveCompact() {
85468
86199
  compactDispatching = false;
85469
86200
  });
85470
86201
  }
85471
- var lastIdleActivityAt = Date.now();
85472
- var lastIdleTurnEndAt = null;
85473
- var idleAutoCleared = false;
85474
- var idleClearDispatching = false;
86202
+ var idleTracker = new IdleTracker(Date.now());
85475
86203
  function markIdleActivity() {
85476
- lastIdleActivityAt = Date.now();
85477
- idleAutoCleared = false;
85478
- }
85479
- function markIdleTurnEnd() {
85480
- lastIdleTurnEndAt = Date.now();
86204
+ idleTracker.noteInbound(Date.now());
85481
86205
  }
85482
86206
  function resolveIdleClearMs() {
85483
86207
  const env = process.env.SWITCHROOM_IDLE_CLEAR_MS;
@@ -85502,30 +86226,39 @@ function resolveIdleClearMs() {
85502
86226
  }
85503
86227
  }
85504
86228
  function maybeIdleClear() {
85505
- if (idleClearDispatching)
86229
+ if (idleTracker.isDispatching)
85506
86230
  return;
85507
86231
  const agentName3 = process.env.SWITCHROOM_AGENT_NAME;
85508
86232
  if (!agentName3)
85509
86233
  return;
85510
86234
  const idleClearMs = resolveIdleClearMs();
85511
- const decision = decideIdleClear({
85512
- lastActivityAt: lastIdleActivityAt,
85513
- lastTurnEndedAt: lastIdleTurnEndAt,
86235
+ const decision = idleTracker.decide(Date.now(), {
85514
86236
  idleClearMs,
85515
- alreadyCleared: idleAutoCleared,
85516
- turnInFlight: turnInFlightForGate()
85517
- }, Date.now());
86237
+ turnInFlight: turnInFlightForGate(),
86238
+ backgroundWorkInFlight: anyPendingAsyncDispatchWithin(BACKGROUND_WORK_SUPPRESS_TTL_MS)
86239
+ });
85518
86240
  if (!decision.clear)
85519
86241
  return;
85520
- idleAutoCleared = true;
85521
- idleClearDispatching = true;
86242
+ idleTracker.markClearFired();
86243
+ idleTracker.beginDispatch();
85522
86244
  process.stderr.write(`telegram gateway: idle auto-/clear for ${agentName3} (idle >= ${Math.round(idleClearMs / 60000)}m)
85523
86245
  `);
85524
- injectSlashCommand(agentName3, "/clear").catch((err) => {
86246
+ const stillIdleAtWrite = () => idleTracker.decideIgnoringLatch(Date.now(), {
86247
+ idleClearMs: resolveIdleClearMs(),
86248
+ turnInFlight: turnInFlightForGate(),
86249
+ backgroundWorkInFlight: anyPendingAsyncDispatchWithin(BACKGROUND_WORK_SUPPRESS_TTL_MS)
86250
+ }).clear;
86251
+ injectSlashCommand(agentName3, "/clear", { precondition: stillIdleAtWrite }).then((result) => {
86252
+ if (result.outcome === "skipped") {
86253
+ idleTracker.reArm();
86254
+ process.stderr.write(`telegram gateway: idle /clear suppressed for ${agentName3} (activity in check-to-send gap)
86255
+ `);
86256
+ }
86257
+ }).catch((err) => {
85525
86258
  process.stderr.write(`telegram gateway: idle /clear inject failed for ${agentName3}: ${err instanceof Error ? err.message : String(err)}
85526
86259
  `);
85527
86260
  }).finally(() => {
85528
- idleClearDispatching = false;
86261
+ idleTracker.endDispatch();
85529
86262
  });
85530
86263
  }
85531
86264
  async function postCompactCard(occ, cap) {
@@ -86905,6 +87638,7 @@ var STREAM_THROTTLE_MS_OVERRIDE = (() => {
86905
87638
  return Number.isFinite(n) && n >= 0 ? n : undefined;
86906
87639
  })();
86907
87640
  var TURN_FLUSH_SAFETY_ENABLED = isTurnFlushSafetyEnabled();
87641
+ var ANSWER_READY_FLUSH_MS2 = resolveAnswerReadyFlushMs(process.env);
86908
87642
  var ANSWER_STREAM_VISIBLE_ENABLED = parseVisibleAnswerStreamEnabled(process.env.SWITCHROOM_VISIBLE_ANSWER_STREAM);
86909
87643
  var ANSWER_LANE = resolveAnswerLaneConfig({
86910
87644
  visibleEnabled: ANSWER_STREAM_VISIBLE_ENABLED
@@ -87384,6 +88118,7 @@ var SILENCE_FALLBACK_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FALLBACK_MS", 3
87384
88118
  var SILENCE_FALLBACK_HARD_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FALLBACK_HARD_MS", 900000);
87385
88119
  var SILENCE_FLOOR_MS = parsePositiveMsEnv("SWITCHROOM_SILENCE_FLOOR_MS", 45000);
87386
88120
  var LIVENESS_TERMINAL_HONESTY = process.env.SWITCHROOM_TG_TERMINAL_HONESTY !== "0";
88121
+ var CAPTURED_PROSE_DELIVERY_ENABLED = process.env.SWITCHROOM_TG_CAPTURED_PROSE_DELIVERY !== "0";
87387
88122
  var SILENCE_DEFER_INFLIGHT_TOOLS = process.env.SWITCHROOM_SILENCE_DEFER_INFLIGHT_TOOLS === "1";
87388
88123
  var SILENCE_LIVENESS_PRODUCTION = process.env.SWITCHROOM_SILENCE_LIVENESS_PRODUCTION !== "0";
87389
88124
  var MEMORY_LEGIBILITY_ENABLED = isMemoryLegibilityEnabled(process.env.SWITCHROOM_MEMORY_LEGIBILITY);
@@ -87698,6 +88433,103 @@ function agentHasInFlightBackgroundWork(now) {
87698
88433
  return ageMs != null && ageMs < TURN_ACTIVE_MARKER_FRESH_MS;
87699
88434
  }
87700
88435
  var lastBgWorkDeferLogMs = 0;
88436
+ async function deliverCapturedProse(args) {
88437
+ const { chatId, threadId, statusKeyStr, registryKey, originTurnId, text: text5, turnDurationMs } = args;
88438
+ const now = Date.now();
88439
+ let outcome;
88440
+ const already = outboundDedup.check(chatId, threadId, text5, now, registryKey);
88441
+ if (already == null) {
88442
+ let out = normalizeParagraphBreaks2(repairEscapedWhitespace2(text5));
88443
+ out = redactOutboundText(out, "captured_prose");
88444
+ const chunks = splitMarkdownChunks2(out, RICH_MESSAGE_MAX_CHARS2);
88445
+ const sentIds = [];
88446
+ try {
88447
+ let liveThreadId = threadId;
88448
+ for (const c of chunks) {
88449
+ const sent = await retryWithThreadFallback2(robustApiCall, (tid) => {
88450
+ const opts = {
88451
+ link_preview_options: { is_disabled: true },
88452
+ ...tid != null ? { message_thread_id: tid } : {}
88453
+ };
88454
+ return bot.api.sendRichMessage(chatId, richMessage2(c), opts);
88455
+ }, { threadId: liveThreadId, chat_id: chatId, verb: "captured-prose.sendMessage" });
88456
+ if (liveThreadId != null && sent.message_thread_id == null) {
88457
+ liveThreadId = undefined;
88458
+ }
88459
+ sentIds.push(sent.message_id);
88460
+ }
88461
+ if (HISTORY_ENABLED && sentIds.length > 0) {
88462
+ try {
88463
+ recordOutbound({
88464
+ chat_id: chatId,
88465
+ thread_id: threadId ?? null,
88466
+ message_ids: sentIds,
88467
+ texts: chunks
88468
+ });
88469
+ } catch {}
88470
+ }
88471
+ outboundDedup.record(chatId, threadId, text5, now, registryKey);
88472
+ process.stderr.write(`telegram gateway: captured-prose delivery \u2014 sent ${out.length} chars recovered from transcript scan (chat=${chatId} origin=${originTurnId})
88473
+ `);
88474
+ outcome = "sent";
88475
+ } catch (err) {
88476
+ 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})
88477
+ `);
88478
+ outcome = "failed";
88479
+ }
88480
+ } else {
88481
+ 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
88482
+ `);
88483
+ outcome = "skipped-dedup";
88484
+ }
88485
+ const settlement = settleCapturedProseDelivery(outcome, {
88486
+ closeObligation: () => {
88487
+ if (OBLIGATION_LEDGER_ENABLED) {
88488
+ try {
88489
+ obligationLedger.close(originTurnId);
88490
+ } catch {}
88491
+ }
88492
+ },
88493
+ clearState: () => clearSilentEndState(statusKeyStr),
88494
+ recordUndelivered: () => {
88495
+ try {
88496
+ const silentEndDeps = HISTORY_ENABLED ? {
88497
+ hasOutboundDeliveredSince: (cid, sinceMs, tid) => hasOutboundDeliveredSince(cid, sinceMs, tid, 1)
88498
+ } : undefined;
88499
+ return recordUndeliveredTurnEnd({ chatId, threadId: threadId ?? null, turnKey: statusKeyStr }, silentEndDeps);
88500
+ } catch (netErr) {
88501
+ process.stderr.write(`telegram gateway: captured-prose recovery-net arm failed: ${netErr.message} (chat=${chatId} origin=${originTurnId})
88502
+ `);
88503
+ return { exhausted: false };
88504
+ }
88505
+ }
88506
+ });
88507
+ if (outcome === "failed" && settlement.exhausted) {
88508
+ 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})
88509
+ `);
88510
+ const plain = redactOutboundText(text5, "captured_prose");
88511
+ const plainChunks = splitMarkdownChunks2(plain, RICH_MESSAGE_MAX_CHARS2);
88512
+ try {
88513
+ let liveThreadId = threadId;
88514
+ for (const c of plainChunks) {
88515
+ 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" });
88516
+ if (liveThreadId != null && sent.message_thread_id == null) {
88517
+ liveThreadId = undefined;
88518
+ }
88519
+ }
88520
+ outboundDedup.record(chatId, threadId, text5, Date.now(), registryKey);
88521
+ process.stderr.write(`telegram gateway: captured-prose recovered via plain-text fallback (chat=${chatId} origin=${originTurnId})
88522
+ `);
88523
+ } catch (plainErr) {
88524
+ 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})
88525
+ `);
88526
+ 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) => {
88527
+ process.stderr.write(`telegram gateway: captured-prose apology fallback send failed: ${err instanceof Error ? err.message : String(err)}
88528
+ `);
88529
+ });
88530
+ }
88531
+ }
88532
+ }
87701
88533
  function obligationSweep() {
87702
88534
  if (!OBLIGATION_LEDGER_ENABLED)
87703
88535
  return;
@@ -88475,7 +89307,8 @@ var ipcServer = createIpcServer({
88475
89307
  swallowingApiCall(() => bot.api.editMessageText(operator, msg.messageId, richMessage2(msg.text), {}), { chat_id: String(operator), verb: "rollout-status-edit" });
88476
89308
  },
88477
89309
  onInjectInbound(_client, msg) {
88478
- markIdleActivity();
89310
+ if (!isCronInjectFire(msg.inbound.meta))
89311
+ markIdleActivity();
88479
89312
  const promptKey = typeof msg.inbound.meta?.prompt_key === "string" ? msg.inbound.meta.prompt_key : "unknown";
88480
89313
  const source = typeof msg.inbound.meta?.source === "string" ? msg.inbound.meta.source : "unknown";
88481
89314
  const isDurableReplay = inboundSpool != null && typeof msg.inbound.meta?.replay_fire_ms === "string" && msg.inbound.meta.replay_fire_ms.length > 0;
@@ -90488,7 +91321,7 @@ async function executeEditMessage(args) {
90488
91321
  editRawText = normalizeParagraphBreaks2(editRawText);
90489
91322
  editRawText = redactOutboundText(editRawText, "edit_message");
90490
91323
  if (!editLiteralText)
90491
- editRawText = stripExcessBold2(normalizePunctuation2(editRawText));
91324
+ editRawText = addParagraphSpacers2(stripExcessBold2(normalizePunctuation2(editRawText)));
90492
91325
  {
90493
91326
  const scrub = scrubVoice2(editRawText);
90494
91327
  if (scrub.replaced > 0) {
@@ -90703,6 +91536,33 @@ function resetOrphanedReplyTimeout() {
90703
91536
  }, ORPHANED_REPLY_TIMEOUT_MS);
90704
91537
  }
90705
91538
  }
91539
+ var answerReadyFlush = new AnswerReadyFlushController({
91540
+ getCurrentTurn: () => currentTurn,
91541
+ getArmInput: (turn) => ({
91542
+ flush: {
91543
+ chatId: turn.sessionChatId,
91544
+ replyCalled: turn.replyCalled,
91545
+ capturedText: turn.capturedText,
91546
+ flushEnabled: TURN_FLUSH_SAFETY_ENABLED
91547
+ },
91548
+ inFlightToolCount: toolFlightTracker.inFlightCount(),
91549
+ hasPendingAsyncDispatch: hasPendingAsyncDispatch(statusKey(turn.sessionChatId, turn.sessionThreadId)),
91550
+ flushWindowMs: ANSWER_READY_FLUSH_MS2
91551
+ }),
91552
+ getTimerHandle: (turn) => turn.answerReadyFlushTimeoutId,
91553
+ setTimerHandle: (turn, handle) => {
91554
+ turn.answerReadyFlushTimeoutId = handle;
91555
+ },
91556
+ onFlush: () => handleSessionEvent({ kind: "turn_end", durationMs: -1, reason: "answer-ready-quiescence" }),
91557
+ log: (msg) => process.stderr.write(`telegram gateway: ${msg}
91558
+ `)
91559
+ });
91560
+ function clearAnswerReadyFlushTimeout(turn) {
91561
+ answerReadyFlush.clear(turn);
91562
+ }
91563
+ function resetAnswerReadyFlushTimeout() {
91564
+ answerReadyFlush.reset();
91565
+ }
90706
91566
  function closeActivityLane(chatId, threadId) {
90707
91567
  const key = chatKeyWithSuffix2(chatId, threadId, "activity");
90708
91568
  const stream = activeDraftStreams.get(key);
@@ -90736,6 +91596,45 @@ function composeTurnActivity(turn, final = false, liveSuffix = "") {
90736
91596
  };
90737
91597
  return renderActivityFeedWithNested2(turn.mirrorLines, childLines, final, liveSuffix, stepCount, header);
90738
91598
  }
91599
+ function retractNarrativeLine(turn, text5) {
91600
+ const clipped = clipNarrative2(text5);
91601
+ const idx = turn.mirrorLines.lastIndexOf(clipped);
91602
+ if (idx === -1)
91603
+ return;
91604
+ turn.mirrorLines.splice(idx, 1);
91605
+ const rerender = composeTurnActivity(turn);
91606
+ if (rerender == null)
91607
+ return;
91608
+ turn.activityPendingRender = rerender;
91609
+ const ea = emissionAuthorityFor(turn);
91610
+ cardDrainGate(turn, ea, () => {
91611
+ if (ea.mayDrain(turn)) {
91612
+ ea.openOrEditCard("narrative", () => {
91613
+ turn.activityInFlight = drainActivitySummary(turn, "narrative");
91614
+ });
91615
+ }
91616
+ });
91617
+ }
91618
+ function makeNarrativeGate(turn) {
91619
+ let handle = null;
91620
+ return new NarrativeFlushController({
91621
+ show: (text5) => showNarrativeStep(turn, text5),
91622
+ retractShown: (text5) => retractNarrativeLine(turn, text5)
91623
+ }, {
91624
+ arm: (fn, ms) => {
91625
+ if (handle != null)
91626
+ clearTimeout(handle);
91627
+ handle = setTimeout(fn, ms);
91628
+ handle.unref?.();
91629
+ },
91630
+ disarm: () => {
91631
+ if (handle != null) {
91632
+ clearTimeout(handle);
91633
+ handle = null;
91634
+ }
91635
+ }
91636
+ }, PENDING_NARRATIVE_FLUSH_MS);
91637
+ }
90739
91638
  function showNarrativeStep(turn, text5) {
90740
91639
  const rendered = appendActivityLabel(turn.mirrorLines, clipNarrative2(text5));
90741
91640
  if (rendered == null)
@@ -90751,31 +91650,13 @@ function showNarrativeStep(turn, text5) {
90751
91650
  });
90752
91651
  }
90753
91652
  function resolvePendingNarrativeOnTool(turn, toolName, input) {
90754
- const pending2 = turn.pendingNarrative;
90755
- if (pending2 == null)
90756
- return;
90757
- turn.pendingNarrative = null;
90758
- if (REPLY_TOOLS.has(toolName)) {
90759
- const replyText = typeof input?.text === "string" ? input.text : "";
90760
- if (isDraftOfReply(pending2.text, replyText))
90761
- return;
90762
- }
90763
- showNarrativeStep(turn, pending2.text);
91653
+ turn.narrativeGate.resolveOnTool(toolName, input);
90764
91654
  }
90765
91655
  function stagePendingNarrative(turn, text5) {
90766
- if (turn.pendingNarrative != null) {
90767
- showNarrativeStep(turn, turn.pendingNarrative.text);
90768
- }
90769
- turn.pendingNarrative = { text: text5 };
91656
+ turn.narrativeGate.stage(text5);
90770
91657
  }
90771
91658
  function flushPendingNarrativeAtTurnEnd(turn, lastReplyText) {
90772
- const pending2 = turn.pendingNarrative;
90773
- if (pending2 == null)
90774
- return;
90775
- turn.pendingNarrative = null;
90776
- if (lastReplyText.length > 0 && isDraftOfReply(pending2.text, lastReplyText))
90777
- return;
90778
- showNarrativeStep(turn, pending2.text);
91659
+ turn.narrativeGate.flushAtTurnEnd(lastReplyText);
90779
91660
  }
90780
91661
  async function drainActivitySummary(turn, producer = "tool", openFlags) {
90781
91662
  try {
@@ -91115,11 +91996,7 @@ function handleSessionEvent(ev) {
91115
91996
  }
91116
91997
  {
91117
91998
  const durationMs = ev.kind === "turn_end" ? ev.durationMs : undefined;
91118
- const signal = classifyIdleEvent(ev.kind, durationMs);
91119
- if (signal.activity)
91120
- markIdleActivity();
91121
- if (signal.turnEnded)
91122
- markIdleTurnEnd();
91999
+ idleTracker.noteEvent(ev.kind, Date.now(), durationMs);
91123
92000
  }
91124
92001
  switch (ev.kind) {
91125
92002
  case "enqueue": {
@@ -91139,6 +92016,7 @@ function handleSessionEvent(ev) {
91139
92016
  clearTimeout(prior.orphanedReplyTimeoutId);
91140
92017
  prior.orphanedReplyTimeoutId = null;
91141
92018
  }
92019
+ prior?.narrativeGate?.teardown();
91142
92020
  const startedAt = Date.now();
91143
92021
  const enqThreadIdNum = ev.threadId != null ? Number(ev.threadId) : undefined;
91144
92022
  const turnId = deriveTurnId(ev.chatId, enqThreadIdNum ?? null, ev.messageId) ?? `${chatKey2(ev.chatId, enqThreadIdNum ?? null)}#synthetic-${startedAt}`;
@@ -91164,6 +92042,7 @@ function handleSessionEvent(ev) {
91164
92042
  silentAnchorText: "",
91165
92043
  capturedText: [],
91166
92044
  orphanedReplyTimeoutId: null,
92045
+ answerReadyFlushTimeoutId: null,
91167
92046
  liveness: new LivenessTracker(startedAt),
91168
92047
  turnId,
91169
92048
  registryKey: null,
@@ -91179,13 +92058,14 @@ function handleSessionEvent(ev) {
91179
92058
  activityEverOpened: false,
91180
92059
  activityDrainFailures: 0,
91181
92060
  mirrorLines: [],
91182
- pendingNarrative: null,
92061
+ narrativeGate: undefined,
91183
92062
  lastReplyText: "",
91184
92063
  foregroundSubAgents: new Map,
91185
92064
  answerStream: null,
91186
92065
  isDm: isDmChatId2(ev.chatId),
91187
92066
  emissionAuthority: new EmissionAuthority(statusKey(ev.chatId, enqThreadIdNum))
91188
92067
  };
92068
+ next.narrativeGate = makeNarrativeGate(next);
91189
92069
  setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
91190
92070
  scheduleEarlyLivenessOpen(next);
91191
92071
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
@@ -91257,6 +92137,7 @@ function handleSessionEvent(ev) {
91257
92137
  const turn = currentTurn;
91258
92138
  if (turn == null)
91259
92139
  return;
92140
+ clearAnswerReadyFlushTimeout(turn);
91260
92141
  resolvePendingNarrativeOnTool(turn, ev.toolName, ev.input);
91261
92142
  turn.toolCallCount++;
91262
92143
  touchTurnActiveMarker2(STATE_DIR);
@@ -91291,6 +92172,7 @@ function handleSessionEvent(ev) {
91291
92172
  const turn = currentTurn;
91292
92173
  if (turn == null)
91293
92174
  return;
92175
+ clearAnswerReadyFlushTimeout(turn);
91294
92176
  resetOrphanedReplyTimeout();
91295
92177
  if (isTelegramSurfaceTool2(ev.toolName))
91296
92178
  return;
@@ -91400,6 +92282,7 @@ function handleSessionEvent(ev) {
91400
92282
  preambleSuppressor.onText(ev.text);
91401
92283
  }
91402
92284
  resetOrphanedReplyTimeout();
92285
+ resetAnswerReadyFlushTimeout();
91403
92286
  if (isContextExhaustionText(ev.text) && turn != null) {
91404
92287
  const chatId = turn.sessionChatId;
91405
92288
  const threadId = turn.sessionThreadId;
@@ -91451,7 +92334,7 @@ function handleSessionEvent(ev) {
91451
92334
  return;
91452
92335
  }
91453
92336
  case "turn_end": {
91454
- if (ev.durationMs === -1) {
92337
+ if (ev.durationMs === -1 && ev.reason !== "answer-ready-quiescence") {
91455
92338
  const turn = currentTurn;
91456
92339
  const key = turn != null ? statusKey(turn.sessionChatId, turn.sessionThreadId) : "";
91457
92340
  const recentlyStreaming = turn != null && turn.liveness.recentlyStreaming(Date.now(), ORPHANED_REPLY_STREAM_WINDOW_MS);
@@ -91525,7 +92408,7 @@ function handleSessionEvent(ev) {
91525
92408
  const chatId = turn.sessionChatId;
91526
92409
  const threadId = turn.sessionThreadId;
91527
92410
  const ctrl = activeStatusReactions.get(statusKey(chatId, threadId));
91528
- const flushDecision = streamFinalizedAsAnswer ? { kind: "skip", reason: "reply-called" } : decideTurnFlush({
92411
+ const flushDecision = streamFinalizedAsAnswer ? { kind: "skip", reason: "reply-called" } : decideTurnFlush2({
91529
92412
  chatId: turn.sessionChatId,
91530
92413
  replyCalled: turn.replyCalled,
91531
92414
  capturedText: turn.capturedText,
@@ -91635,7 +92518,7 @@ function handleSessionEvent(ev) {
91635
92518
  }) ?? { wasEmitted: false, turnKey: null };
91636
92519
  const backstopCardMessageId = cardTakeover.wasEmitted && cardTakeover.turnKey != null ? getPinnedProgressCardMessageId?.(cardTakeover.turnKey) ?? null : null;
91637
92520
  const backstopCardTurnKey = cardTakeover.turnKey;
91638
- endCurrentTurnAtomic(turn);
92521
+ const backstopTurnEndedAt = endCurrentTurnAtomic(turn, { deferRecord: true });
91639
92522
  preambleSuppressor.dropNow();
91640
92523
  {
91641
92524
  const tKey = statusKey(chatId, threadId);
@@ -91652,6 +92535,10 @@ function handleSessionEvent(ev) {
91652
92535
  if (recentCount > 0) {
91653
92536
  process.stderr.write(`telegram gateway: turn-flush suppressed \u2014 reply tool sent ${recentCount} message(s) within 2s
91654
92537
  `);
92538
+ if (backstopTurnEndedAt != null) {
92539
+ turn.deliveryOutcome = "suppressed";
92540
+ emitTurnRecord(turn, backstopTurnEndedAt);
92541
+ }
91655
92542
  return;
91656
92543
  }
91657
92544
  } catch {}
@@ -91663,10 +92550,12 @@ function handleSessionEvent(ev) {
91663
92550
  link_preview_options: { is_disabled: true }
91664
92551
  };
91665
92552
  const limit = RICH_MESSAGE_MAX_CHARS2;
91666
- const renderedText = capturedText;
91667
- const htmlChunks = splitMarkdownChunks2(renderedText, limit);
92553
+ let htmlChunks = [];
91668
92554
  const sentIds = [];
92555
+ let sendThrew = false;
91669
92556
  try {
92557
+ const renderedText = addParagraphSpacers2(capturedText);
92558
+ htmlChunks = splitMarkdownChunks2(renderedText, limit);
91670
92559
  let firstSendUsedEdit = false;
91671
92560
  let liveThreadId = backstopThreadId;
91672
92561
  if (backstopCardMessageId != null && htmlChunks.length > 0) {
@@ -91729,10 +92618,20 @@ function handleSessionEvent(ev) {
91729
92618
  unpinProgressCardForChat?.(backstopChatId, backstopThreadId);
91730
92619
  }
91731
92620
  } catch (err) {
92621
+ sendThrew = true;
91732
92622
  process.stderr.write(`telegram gateway: turn-flush send failed: ${err.message}
91733
92623
  `);
91734
92624
  if (backstopCtrl)
91735
92625
  backstopCtrl.finalize("error");
92626
+ } finally {
92627
+ if (backstopTurnEndedAt != null) {
92628
+ finalizeBackstopSend(turn, {
92629
+ threw: sendThrew,
92630
+ sentCount: sentIds.length,
92631
+ chunkCount: htmlChunks.length
92632
+ });
92633
+ emitTurnRecord(turn, backstopTurnEndedAt);
92634
+ }
91736
92635
  }
91737
92636
  })();
91738
92637
  return;
@@ -91774,21 +92673,40 @@ function handleSessionEvent(ev) {
91774
92673
  ended_via: outboundMetrics.outboundCount > 0 ? "reply" : "silent"
91775
92674
  });
91776
92675
  if (turnEndDecision === "reprompt") {
91777
- const silentEndDeps = HISTORY_ENABLED ? {
91778
- hasOutboundDeliveredSince: (cid, sinceMs, tid) => hasOutboundDeliveredSince(cid, sinceMs, tid, 1)
91779
- } : undefined;
91780
- const silentEnd = recordUndeliveredTurnEnd({
91781
- chatId,
91782
- threadId: threadId ?? null,
91783
- turnKey: tKey
91784
- }, silentEndDeps);
91785
- if (silentEnd.exhausted) {
91786
- process.stderr.write(`telegram gateway: WARN silent-end fallback \u2014 agent stayed ` + `silent after the Stop-hook re-prompt; delivering fallback message chat=${chatId} turnKey=${tKey} (#1161)
91787
- `);
91788
- retryWithThreadFallback2(robustApiCall, (tid) => bot.api.sendMessage(chatId, silentEndFallbackText(turnDurationMs), tid != null ? { message_thread_id: tid } : {}), { threadId, chat_id: chatId, verb: "silent-end-fallback.sendMessage" }).catch((err) => {
91789
- process.stderr.write(`telegram gateway: silent-end fallback send failed: ${err instanceof Error ? err.message : String(err)}
92676
+ const proseDecision = CAPTURED_PROSE_DELIVERY_ENABLED ? decideCapturedProseDelivery({
92677
+ turnKey: tKey,
92678
+ turnId: turn.turnId,
92679
+ minChars: CAPTURED_PROSE_MIN_CHARS
92680
+ }) : { deliver: false, reason: "no-state" };
92681
+ if (proseDecision.deliver && proseDecision.text != null) {
92682
+ process.stderr.write(`telegram gateway: captured-prose delivery engaged on first silent-end chat=${chatId} turnKey=${tKey} (#3227)
91790
92683
  `);
92684
+ deliverCapturedProse({
92685
+ chatId,
92686
+ threadId,
92687
+ statusKeyStr: tKey,
92688
+ registryKey: turn.registryKey ?? null,
92689
+ originTurnId: turn.turnId,
92690
+ text: proseDecision.text,
92691
+ turnDurationMs
91791
92692
  });
92693
+ } else {
92694
+ const silentEndDeps = HISTORY_ENABLED ? {
92695
+ hasOutboundDeliveredSince: (cid, sinceMs, tid) => hasOutboundDeliveredSince(cid, sinceMs, tid, 1)
92696
+ } : undefined;
92697
+ const silentEnd = recordUndeliveredTurnEnd({
92698
+ chatId,
92699
+ threadId: threadId ?? null,
92700
+ turnKey: tKey
92701
+ }, silentEndDeps);
92702
+ if (silentEnd.exhausted) {
92703
+ 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)
92704
+ `);
92705
+ 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) => {
92706
+ process.stderr.write(`telegram gateway: silent-end fallback send failed: ${err instanceof Error ? err.message : String(err)}
92707
+ `);
92708
+ });
92709
+ }
91792
92710
  }
91793
92711
  }
91794
92712
  clear(tKey);
@@ -98269,6 +99187,7 @@ var didOneTimeSetup = false;
98269
99187
  },
98270
99188
  floodWaitRemainingMs: probeFloodWaitRemainingMs,
98271
99189
  maxRows: workerFeedMaxRows,
99190
+ staleWorkerTtlMs: resolveInflightTerminalCapMs() + WORKER_FEED_STALE_TTL_MARGIN_MS,
98272
99191
  reconcilePin: ({ feedKey, chatId, messageId }) => {
98273
99192
  if (!PIN_STATUS_WHILE_WORKING)
98274
99193
  return;
@@ -98314,6 +99233,14 @@ var didOneTimeSetup = false;
98314
99233
  process.stderr.write(`telegram gateway: worker ${agentId} NAMED AS LOST \u2014 falsely finalised twice, resurrection chain bound reached (issue #3023)
98315
99234
  `);
98316
99235
  },
99236
+ onTerminalCleanup: (agentId) => {
99237
+ try {
99238
+ workerActivityFeed?.terminate(agentId);
99239
+ } catch (err) {
99240
+ process.stderr.write(`telegram gateway: worker terminal-cleanup feed removal error agent=${agentId}: ${err.message}
99241
+ `);
99242
+ }
99243
+ },
98317
99244
  onFinish: ({ agentId, outcome, description, resultText, toolCount, durationMs, background: entryBackground }) => {
98318
99245
  deferredDoneReactions.promote();
98319
99246
  let fleetChatId = "";
@@ -98442,7 +99369,7 @@ var didOneTimeSetup = false;
98442
99369
  process.stderr.write(`telegram gateway: subagent-handback queued agent=${agentId} outcome=${outcome} chat=${decision.chatId} resultChars=${resultText.length}
98443
99370
  `);
98444
99371
  },
98445
- onProgress: ({ agentId, description, latestSummary, elapsedMs, prevBucketIdx, setBucketIdx, lastTool, toolCount, progressLine, model }) => {
99372
+ onProgress: ({ agentId, description, latestSummary, elapsedMs, prevBucketIdx, setBucketIdx, lastTool, toolCount, progressLine, model, skeleton }) => {
98446
99373
  let fleetChatId = "";
98447
99374
  try {
98448
99375
  const fleets = progressDriver?.peekAllFleets() ?? [];
@@ -98485,6 +99412,8 @@ var didOneTimeSetup = false;
98485
99412
  }
98486
99413
  if (surface !== "nest")
98487
99414
  return;
99415
+ if (skeleton)
99416
+ return;
98488
99417
  const turn = currentTurn;
98489
99418
  if (turn == null)
98490
99419
  return;
@@ -98544,6 +99473,7 @@ var didOneTimeSetup = false;
98544
99473
  }
98545
99474
  const progressOrigin = resolveSubagentOriginChat(agentId);
98546
99475
  const decision = decideSubagentProgress({
99476
+ skeleton: skeleton === true,
98547
99477
  disableEnvValue: process.env.SWITCHROOM_DISABLE_SUBAGENT_PROGRESS,
98548
99478
  isBackground,
98549
99479
  fleetChatId: progressOrigin?.chatId || fleetChatId,