switchroom 0.18.25 → 0.18.27

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 (44) hide show
  1. package/README.md +6 -2
  2. package/dist/cli/ms-365-write-pretool.mjs +4953 -14
  3. package/dist/cli/switchroom.js +1 -1
  4. package/dist/host-control/main.js +1 -1
  5. package/package.json +2 -2
  6. package/profiles/_base/start.sh.hbs +16 -0
  7. package/telegram-plugin/dist/gateway/gateway.js +832 -38
  8. package/telegram-plugin/flushed-turn-supersede.ts +58 -0
  9. package/telegram-plugin/gateway/derive-turn-id.ts +32 -0
  10. package/telegram-plugin/gateway/gateway.ts +305 -41
  11. package/telegram-plugin/gateway/handback-preturn-signal.ts +442 -0
  12. package/telegram-plugin/gateway/model-command.ts +68 -0
  13. package/telegram-plugin/gateway/ms365-write-approval.test.ts +101 -0
  14. package/telegram-plugin/gateway/ms365-write-approval.ts +65 -3
  15. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +12 -0
  16. package/telegram-plugin/gateway/turn-active-marker.ts +35 -0
  17. package/telegram-plugin/render/code-segments.ts +210 -0
  18. package/telegram-plugin/render/dollar-math-guard.ts +126 -0
  19. package/telegram-plugin/render/emphasis-guard.ts +158 -0
  20. package/telegram-plugin/render/inline-pairs-guard.ts +171 -0
  21. package/telegram-plugin/render/line-start-guard.ts +167 -0
  22. package/telegram-plugin/render/rich-render.ts +7 -0
  23. package/telegram-plugin/rich-send.ts +48 -2
  24. package/telegram-plugin/send-gate.test.ts +138 -0
  25. package/telegram-plugin/send-gate.ts +104 -1
  26. package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +14 -4
  27. package/telegram-plugin/tests/effort-command.test.ts +47 -0
  28. package/telegram-plugin/tests/flushed-turn-supersede.test.ts +60 -0
  29. package/telegram-plugin/tests/handback-preturn-adoption-roundtrip.test.ts +211 -0
  30. package/telegram-plugin/tests/handback-preturn-signal.test.ts +346 -0
  31. package/telegram-plugin/tests/model-command.test.ts +112 -0
  32. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +14 -2
  33. package/telegram-plugin/tests/outbound-send-chunks.test.ts +57 -0
  34. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +18 -11
  35. package/telegram-plugin/tests/render/dollar-math-guard.test.ts +162 -0
  36. package/telegram-plugin/tests/render/emphasis-guard.test.ts +205 -0
  37. package/telegram-plugin/tests/render/guard-composition.test.ts +138 -0
  38. package/telegram-plugin/tests/render/inline-pairs-guard.test.ts +171 -0
  39. package/telegram-plugin/tests/render/line-start-guard.test.ts +164 -0
  40. package/telegram-plugin/tests/reply-owner-resolve.test.ts +90 -0
  41. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +5 -0
  42. package/telegram-plugin/tests/turn-active-marker.test.ts +29 -0
  43. package/telegram-plugin/tests/worker-activity-feed.test.ts +121 -0
  44. package/telegram-plugin/worker-activity-feed.ts +91 -1
@@ -7525,9 +7525,329 @@ var init_approval_card = __esm(() => {
7525
7525
  import_grammy3 = __toESM(require_mod2(), 1);
7526
7526
  });
7527
7527
 
7528
+ // render/code-segments.ts
7529
+ function findClosingBackticks(text, from, runLen) {
7530
+ let i = from;
7531
+ while (i < text.length) {
7532
+ if (text[i] === "`") {
7533
+ let j = i;
7534
+ while (j < text.length && text[j] === "`")
7535
+ j++;
7536
+ if (j - i === runLen)
7537
+ return j;
7538
+ i = j;
7539
+ } else {
7540
+ i++;
7541
+ }
7542
+ }
7543
+ return -1;
7544
+ }
7545
+ function splitCodeSegments(text) {
7546
+ const out = [];
7547
+ let i = 0;
7548
+ let plainStart = 0;
7549
+ while (i < text.length) {
7550
+ if (text[i] === "`") {
7551
+ let j = i;
7552
+ while (j < text.length && text[j] === "`")
7553
+ j++;
7554
+ const runLen = j - i;
7555
+ const close = findClosingBackticks(text, j, runLen);
7556
+ if (close !== -1) {
7557
+ if (plainStart < i)
7558
+ out.push({ code: false, text: text.slice(plainStart, i) });
7559
+ out.push({ code: true, text: text.slice(i, close) });
7560
+ i = close;
7561
+ plainStart = close;
7562
+ continue;
7563
+ }
7564
+ }
7565
+ i++;
7566
+ }
7567
+ if (plainStart < text.length)
7568
+ out.push({ code: false, text: text.slice(plainStart) });
7569
+ return out;
7570
+ }
7571
+ function isTableDelimiterRow(line) {
7572
+ const t = line.trim();
7573
+ return t.length > 0 && /^[\s|:-]+$/.test(t) && t.includes("-") && t.includes("|");
7574
+ }
7575
+ function isTableCandidateLine(line) {
7576
+ return /^\s*\|/.test(line);
7577
+ }
7578
+ function findTableRanges(text) {
7579
+ const ranges = [];
7580
+ const lines = text.split(`
7581
+ `);
7582
+ let offset = 0;
7583
+ let runStart = -1;
7584
+ let runEnd = -1;
7585
+ let runHasDelim = false;
7586
+ let runLineCount = 0;
7587
+ const flush = () => {
7588
+ if (runStart !== -1 && runLineCount >= 2 && runHasDelim) {
7589
+ ranges.push([runStart, runEnd]);
7590
+ }
7591
+ runStart = -1;
7592
+ runEnd = -1;
7593
+ runHasDelim = false;
7594
+ runLineCount = 0;
7595
+ };
7596
+ for (let k = 0;k < lines.length; k++) {
7597
+ const line = lines[k];
7598
+ const lineLen = line.length + (k < lines.length - 1 ? 1 : 0);
7599
+ if (isTableCandidateLine(line)) {
7600
+ if (runStart === -1)
7601
+ runStart = offset;
7602
+ runEnd = offset + lineLen;
7603
+ runLineCount += 1;
7604
+ if (isTableDelimiterRow(line))
7605
+ runHasDelim = true;
7606
+ } else {
7607
+ flush();
7608
+ }
7609
+ offset += lineLen;
7610
+ }
7611
+ flush();
7612
+ return ranges;
7613
+ }
7614
+ function splitProseProtected(text) {
7615
+ const out = [];
7616
+ const tables = findTableRanges(text);
7617
+ let tIdx = 0;
7618
+ let i = 0;
7619
+ let plainStart = 0;
7620
+ const pushProtected = (from, to) => {
7621
+ if (plainStart < from)
7622
+ out.push({ code: false, text: text.slice(plainStart, from) });
7623
+ out.push({ code: true, text: text.slice(from, to) });
7624
+ plainStart = to;
7625
+ };
7626
+ while (i < text.length) {
7627
+ while (tIdx < tables.length && tables[tIdx][1] <= i)
7628
+ tIdx++;
7629
+ if (tIdx < tables.length && tables[tIdx][0] === i) {
7630
+ const [, end] = tables[tIdx];
7631
+ pushProtected(i, end);
7632
+ i = end;
7633
+ continue;
7634
+ }
7635
+ const ch = text[i];
7636
+ if (ch === "[") {
7637
+ const close = text.indexOf("]", i + 1);
7638
+ if (close !== -1 && text[close + 1] === "(") {
7639
+ const destClose = text.indexOf(")", close + 2);
7640
+ if (destClose !== -1) {
7641
+ pushProtected(close + 1, destClose + 1);
7642
+ i = destClose + 1;
7643
+ continue;
7644
+ }
7645
+ }
7646
+ i++;
7647
+ continue;
7648
+ }
7649
+ if ((ch === "h" || ch === "w") && (i === 0 || !/[A-Za-z0-9]/.test(text[i - 1]))) {
7650
+ const m = /^(?:https?:\/\/|www\.)[^\s<>()\[\]]+/.exec(text.slice(i));
7651
+ if (m) {
7652
+ const end = i + m[0].length;
7653
+ pushProtected(i, end);
7654
+ i = end;
7655
+ continue;
7656
+ }
7657
+ }
7658
+ i++;
7659
+ }
7660
+ if (plainStart < text.length)
7661
+ out.push({ code: false, text: text.slice(plainStart) });
7662
+ return out;
7663
+ }
7664
+ function splitProtectedSegments(text) {
7665
+ const out = [];
7666
+ for (const seg of splitCodeSegments(text)) {
7667
+ if (seg.code) {
7668
+ out.push(seg);
7669
+ continue;
7670
+ }
7671
+ for (const sub of splitProseProtected(seg.text))
7672
+ out.push(sub);
7673
+ }
7674
+ return out;
7675
+ }
7676
+
7677
+ // render/dollar-math-guard.ts
7678
+ function guardDollarMath(text) {
7679
+ if (!text.includes("$"))
7680
+ return text;
7681
+ const segments = splitProtectedSegments(text);
7682
+ let total = 0;
7683
+ let hasCurrencySignal = false;
7684
+ for (const seg of segments) {
7685
+ if (seg.code)
7686
+ continue;
7687
+ total += seg.text.match(ANY_DOLLAR)?.length ?? 0;
7688
+ if (!hasCurrencySignal && CURRENCY_SIGNAL.test(seg.text))
7689
+ hasCurrencySignal = true;
7690
+ }
7691
+ if (total < 2 || !hasCurrencySignal)
7692
+ return text;
7693
+ return segments.map((seg) => seg.code ? seg.text : seg.text.replace(UNESCAPED_DOLLAR, () => "\\$")).join("");
7694
+ }
7695
+ var ANY_DOLLAR, CURRENCY_SIGNAL, UNESCAPED_DOLLAR;
7696
+ var init_dollar_math_guard = __esm(() => {
7697
+ ANY_DOLLAR = /\$/g;
7698
+ CURRENCY_SIGNAL = /\$\.?\d|\d\s?\$/;
7699
+ UNESCAPED_DOLLAR = /(?<!\\)\$/g;
7700
+ });
7701
+
7702
+ // render/emphasis-guard.ts
7703
+ function guardAccidentalEmphasis(text) {
7704
+ if (!text.includes("_") && !text.includes("*"))
7705
+ return text;
7706
+ const segments = splitProtectedSegments(text);
7707
+ let hasIntraUnderscore = false;
7708
+ let hasIntraAsterisk = false;
7709
+ let underscoreCount = 0;
7710
+ let asteriskCount = 0;
7711
+ for (const seg of segments) {
7712
+ if (seg.code)
7713
+ continue;
7714
+ if (INTRA_WORD_UNDERSCORE.test(seg.text))
7715
+ hasIntraUnderscore = true;
7716
+ if (INTRA_WORD_ASTERISK.test(seg.text))
7717
+ hasIntraAsterisk = true;
7718
+ underscoreCount += seg.text.match(ANY_UNDERSCORE)?.length ?? 0;
7719
+ asteriskCount += seg.text.match(ANY_ASTERISK)?.length ?? 0;
7720
+ }
7721
+ INTRA_WORD_UNDERSCORE.lastIndex = 0;
7722
+ INTRA_WORD_ASTERISK.lastIndex = 0;
7723
+ const armUnderscore = hasIntraUnderscore && underscoreCount >= 2;
7724
+ const armAsterisk = hasIntraAsterisk && asteriskCount >= 2;
7725
+ if (!armUnderscore && !armAsterisk)
7726
+ return text;
7727
+ return segments.map((seg) => {
7728
+ if (seg.code)
7729
+ return seg.text;
7730
+ let out = seg.text;
7731
+ if (armUnderscore)
7732
+ out = out.replace(INTRA_WORD_UNDERSCORE, "\\_");
7733
+ if (armAsterisk)
7734
+ out = out.replace(INTRA_WORD_ASTERISK, "\\*");
7735
+ return out;
7736
+ }).join("");
7737
+ }
7738
+ var INTRA_WORD_UNDERSCORE, INTRA_WORD_ASTERISK, ANY_UNDERSCORE, ANY_ASTERISK;
7739
+ var init_emphasis_guard = __esm(() => {
7740
+ INTRA_WORD_UNDERSCORE = /(?<=[A-Za-z0-9])_(?=[A-Za-z0-9])/g;
7741
+ INTRA_WORD_ASTERISK = /(?<=[A-Za-z0-9])\*(?=[A-Za-z0-9])/g;
7742
+ ANY_UNDERSCORE = /(?<!\\)_/g;
7743
+ ANY_ASTERISK = /(?<!\\)\*/g;
7744
+ });
7745
+
7746
+ // render/line-start-guard.ts
7747
+ function escapeAccidentalLineStart(line) {
7748
+ const indent = /^ */.exec(line)[0];
7749
+ if (indent.length >= 4)
7750
+ return line;
7751
+ const rest = line.slice(indent.length);
7752
+ if (ACCIDENTAL_BLOCKQUOTE.test(rest)) {
7753
+ return indent + "\\" + rest;
7754
+ }
7755
+ const ol = ACCIDENTAL_ORDERED_LIST.exec(rest);
7756
+ if (ol) {
7757
+ const digits = ol[1];
7758
+ const delim = ol[2];
7759
+ return indent + digits + "\\" + delim + rest.slice(digits.length + 1);
7760
+ }
7761
+ return line;
7762
+ }
7763
+ function guardAccidentalBlockConstructs(text) {
7764
+ if (!text.includes(">") && !/\d{4,}[.)]/.test(text))
7765
+ return text;
7766
+ const segments = splitProtectedSegments(text);
7767
+ let out = "";
7768
+ let atLineStart = true;
7769
+ for (const seg of segments) {
7770
+ if (seg.code) {
7771
+ out += seg.text;
7772
+ atLineStart = seg.text.endsWith(`
7773
+ `);
7774
+ continue;
7775
+ }
7776
+ const lines = seg.text.split(`
7777
+ `);
7778
+ for (let k = 0;k < lines.length; k++) {
7779
+ const lineIsAtStart = k === 0 ? atLineStart : true;
7780
+ const processed = lineIsAtStart ? escapeAccidentalLineStart(lines[k]) : lines[k];
7781
+ out += processed;
7782
+ if (k < lines.length - 1)
7783
+ out += `
7784
+ `;
7785
+ }
7786
+ atLineStart = seg.text.endsWith(`
7787
+ `);
7788
+ }
7789
+ return out;
7790
+ }
7791
+ var ACCIDENTAL_BLOCKQUOTE, ACCIDENTAL_ORDERED_LIST;
7792
+ var init_line_start_guard = __esm(() => {
7793
+ ACCIDENTAL_BLOCKQUOTE = /^>[0-9=]/;
7794
+ ACCIDENTAL_ORDERED_LIST = /^(\d{4,})([.)])(\s|$)/;
7795
+ });
7796
+
7797
+ // render/inline-pairs-guard.ts
7798
+ function countMatches(text, re) {
7799
+ return text.match(re)?.length ?? 0;
7800
+ }
7801
+ function guardAccidentalInlinePairs(text) {
7802
+ if (!/[~=|]/.test(text))
7803
+ return text;
7804
+ const segments = splitProtectedSegments(text);
7805
+ let tildes = 0;
7806
+ let marks = 0;
7807
+ let spoilers = 0;
7808
+ for (const seg of segments) {
7809
+ if (seg.code)
7810
+ continue;
7811
+ tildes += countMatches(seg.text, TILDE_APPROX);
7812
+ marks += countMatches(seg.text, MARK_OP);
7813
+ spoilers += countMatches(seg.text, SPOILER_OP);
7814
+ }
7815
+ const armTilde = tildes >= 2;
7816
+ const armMark = marks >= 2;
7817
+ const armSpoiler = spoilers >= 2;
7818
+ if (!armTilde && !armMark && !armSpoiler)
7819
+ return text;
7820
+ return segments.map((seg) => {
7821
+ if (seg.code)
7822
+ return seg.text;
7823
+ let out = seg.text;
7824
+ if (armTilde)
7825
+ out = out.replace(TILDE_APPROX, () => "\\~");
7826
+ if (armMark)
7827
+ out = out.replace(MARK_OP, () => "\\=\\=");
7828
+ if (armSpoiler)
7829
+ out = out.replace(SPOILER_OP, () => "\\|\\|");
7830
+ return out;
7831
+ }).join("");
7832
+ }
7833
+ var TILDE_APPROX, MARK_OP, SPOILER_OP;
7834
+ var init_inline_pairs_guard = __esm(() => {
7835
+ TILDE_APPROX = /(?<!\\)~(?=\$?\.?\d)/g;
7836
+ MARK_OP = /(?<=\w)==(?=\w)/g;
7837
+ SPOILER_OP = /(?<=\w)\|\|(?=\w)/g;
7838
+ });
7839
+
7528
7840
  // rich-send.ts
7841
+ function guardAccidentalFormatting(markdown) {
7842
+ let out = markdown;
7843
+ out = guardAccidentalEmphasis(out);
7844
+ out = guardAccidentalBlockConstructs(out);
7845
+ out = guardAccidentalInlinePairs(out);
7846
+ out = guardDollarMath(out);
7847
+ return out;
7848
+ }
7529
7849
  function richMessage(markdown) {
7530
- return { markdown };
7850
+ return { markdown: guardAccidentalFormatting(markdown) };
7531
7851
  }
7532
7852
  function isParseEntitiesError(err) {
7533
7853
  if (!(err instanceof import_grammy4.GrammyError) || err.error_code !== 400)
@@ -7545,6 +7865,10 @@ function isLengthError(err) {
7545
7865
  }
7546
7866
  var import_grammy4;
7547
7867
  var init_rich_send = __esm(() => {
7868
+ init_dollar_math_guard();
7869
+ init_emphasis_guard();
7870
+ init_line_start_guard();
7871
+ init_inline_pairs_guard();
7548
7872
  import_grammy4 = __toESM(require_mod2(), 1);
7549
7873
  });
7550
7874
 
@@ -39346,6 +39670,13 @@ function decideSupersede(record, args) {
39346
39670
  }
39347
39671
  return { supersede: true, deleteMessageIds: [...record.messageIds], reason: "supersede" };
39348
39672
  }
39673
+ function decideSupersedeCorrection(input) {
39674
+ const eligible = input.flushMessageIds.length === 1 && input.chunkCount === 1 && !input.hasFiles && !input.suppressText && !input.hasOpenPreview;
39675
+ if (eligible) {
39676
+ return { mode: "edit-in-place", editMessageId: input.flushMessageIds[0], deleteMessageIds: [] };
39677
+ }
39678
+ return { mode: "delete-resend", deleteMessageIds: [...input.flushMessageIds] };
39679
+ }
39349
39680
  var NULL_TURN_KEY = "<<null-turn>>";
39350
39681
  function turnKey(turnId) {
39351
39682
  return turnId == null ? NULL_TURN_KEY : turnId;
@@ -40638,6 +40969,7 @@ function createWorkerActivityFeed(opts) {
40638
40969
  const nowFn = opts.now ?? Date.now;
40639
40970
  const floodWaitRemainingMs = opts.floodWaitRemainingMs ?? (() => 0);
40640
40971
  const minEditInterval = opts.minEditIntervalMs ?? 2500;
40972
+ const elapsedRefreshMs = Math.max(minEditInterval, Math.floor(opts.elapsedRefreshMs ?? 15000));
40641
40973
  const firstPaintMin = opts.firstPaintMinMs ?? 8000;
40642
40974
  const heartbeatTickMs = opts.heartbeatTickMs ?? 6000;
40643
40975
  const maxRows = Math.max(1, Math.floor(opts.maxRows ?? 8));
@@ -40752,6 +41084,35 @@ function createWorkerActivityFeed(opts) {
40752
41084
  });
40753
41085
  return renderCombinedWorkerFeed(rows, { maxRows });
40754
41086
  }
41087
+ function groupSubstanceKey(g, terminalRecap) {
41088
+ const FS = "\x00";
41089
+ const RS = "\x1E";
41090
+ if (terminalRecap != null) {
41091
+ return [
41092
+ "T",
41093
+ terminalRecap.state,
41094
+ terminalRecap.description,
41095
+ terminalRecap.toolCount,
41096
+ terminalRecap.totalTokens ?? "",
41097
+ terminalRecap.latestSummary,
41098
+ ...terminalRecap.narrativeLines ?? []
41099
+ ].join(FS);
41100
+ }
41101
+ const running = runningRows(g);
41102
+ if (running.length === 0)
41103
+ return "EMPTY";
41104
+ return running.map((r) => {
41105
+ const v = r.lastView;
41106
+ return [
41107
+ r.agentId,
41108
+ v.state,
41109
+ v.description,
41110
+ v.toolCount,
41111
+ v.totalTokens ?? "",
41112
+ ...r.narrative
41113
+ ].join(FS);
41114
+ }).join(RS);
41115
+ }
40755
41116
  function removeWorker(g, agentId) {
40756
41117
  g.workers.delete(agentId);
40757
41118
  agentIndex.delete(agentId);
@@ -40801,6 +41162,7 @@ function createWorkerActivityFeed(opts) {
40801
41162
  return;
40802
41163
  }
40803
41164
  const body = renderGroupBody(g, now, opts2.terminalRecap ?? null, opts2.heartbeat ?? false);
41165
+ const substanceKey = groupSubstanceKey(g, opts2.terminalRecap ?? null);
40804
41166
  if (body == null) {
40805
41167
  if (isTerminal)
40806
41168
  clearStaged();
@@ -40824,6 +41186,7 @@ function createWorkerActivityFeed(opts) {
40824
41186
  g.messageId = sent.message_id;
40825
41187
  g.messageCreatedAtMs = now;
40826
41188
  g.lastBody = body;
41189
+ g.lastSubstanceKey = substanceKey;
40827
41190
  g.lastEditAt = now;
40828
41191
  g.terminalPainted = false;
40829
41192
  syncPin(g);
@@ -40839,8 +41202,13 @@ function createWorkerActivityFeed(opts) {
40839
41202
  clearStaged();
40840
41203
  return;
40841
41204
  }
40842
- if (!opts2.force && now - g.lastEditAt < minEditInterval)
40843
- return;
41205
+ if (!opts2.force && !isTerminal) {
41206
+ if (now - g.lastEditAt < minEditInterval)
41207
+ return;
41208
+ const substanceChanged = substanceKey !== g.lastSubstanceKey;
41209
+ if (!substanceChanged && now - g.lastEditAt < elapsedRefreshMs)
41210
+ return;
41211
+ }
40844
41212
  try {
40845
41213
  const res = await opts.bot.editMessageText(g.chatId, g.messageId, body, sendOptsFor(g));
40846
41214
  if (isSendGateShed(res)) {
@@ -40848,6 +41216,7 @@ function createWorkerActivityFeed(opts) {
40848
41216
  return;
40849
41217
  }
40850
41218
  g.lastBody = body;
41219
+ g.lastSubstanceKey = substanceKey;
40851
41220
  g.lastEditAt = now;
40852
41221
  if (isTerminal) {
40853
41222
  log(`worker-feed: finish feed=${g.feedKey} chat=${g.chatId} thread=${g.threadId ?? "-"} ` + `msgId=${g.messageId} agent=${finishingAgentId ?? "-"} ` + `state=${opts2.terminalRecap?.state ?? "done"} bytes=${body.length}`);
@@ -40865,6 +41234,7 @@ function createWorkerActivityFeed(opts) {
40865
41234
  }
40866
41235
  if (outcome === "not_modified") {
40867
41236
  g.lastBody = body;
41237
+ g.lastSubstanceKey = substanceKey;
40868
41238
  g.lastEditAt = now;
40869
41239
  if (isTerminal)
40870
41240
  clearStaged();
@@ -40874,6 +41244,7 @@ function createWorkerActivityFeed(opts) {
40874
41244
  g.messageId = null;
40875
41245
  g.messageCreatedAtMs = 0;
40876
41246
  g.lastBody = null;
41247
+ g.lastSubstanceKey = null;
40877
41248
  if (isTerminal)
40878
41249
  clearStaged();
40879
41250
  else
@@ -40983,6 +41354,7 @@ function createWorkerActivityFeed(opts) {
40983
41354
  g.messageId = null;
40984
41355
  g.messageCreatedAtMs = 0;
40985
41356
  g.lastBody = null;
41357
+ g.lastSubstanceKey = null;
40986
41358
  syncPin(g);
40987
41359
  opts.bot.editMessageText(g.chatId, retiredId, WORKER_CARD_SUPERSEDED_BODY, sendOptsFor(g)).catch(() => {});
40988
41360
  }
@@ -41042,6 +41414,7 @@ function createWorkerActivityFeed(opts) {
41042
41414
  messageId: null,
41043
41415
  messageCreatedAtMs: 0,
41044
41416
  lastBody: null,
41417
+ lastSubstanceKey: null,
41045
41418
  lastEditAt: 0,
41046
41419
  cooldownUntil: 0,
41047
41420
  chain: Promise.resolve(),
@@ -41055,6 +41428,7 @@ function createWorkerActivityFeed(opts) {
41055
41428
  g.messageId = null;
41056
41429
  g.messageCreatedAtMs = 0;
41057
41430
  g.lastBody = null;
41431
+ g.lastSubstanceKey = null;
41058
41432
  g.pendingFinalize.clear();
41059
41433
  g.terminalPainted = false;
41060
41434
  syncPin(g);
@@ -45869,6 +46243,207 @@ function createTurnTypingLoop(deps) {
45869
46243
  };
45870
46244
  }
45871
46245
 
46246
+ // gateway/handback-preturn-signal.ts
46247
+ var PRETURN_TURNKEY_PREFIX = "preturn:";
46248
+ function isHandbackInbound(msg) {
46249
+ return msg.type === "inbound" && msg.meta?.source === "subagent_handback";
46250
+ }
46251
+ function createHandbackPreturnSignal(deps) {
46252
+ const now = deps.now ?? (() => Date.now());
46253
+ const debounceMs = deps.debounceMs ?? 700;
46254
+ const adoptTimeoutMs = deps.adoptTimeoutMs ?? 30000;
46255
+ const setTimer = deps.setTimer ?? ((fn, ms) => {
46256
+ const t = setTimeout(fn, ms);
46257
+ t.unref?.();
46258
+ return t;
46259
+ });
46260
+ const clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
46261
+ const log = deps.log ?? ((l) => process.stderr.write(l));
46262
+ const byKey = new Map;
46263
+ const bySyntheticKey = new Map;
46264
+ function clearTimers(entry) {
46265
+ if (entry.debounceTimer != null) {
46266
+ clearTimer(entry.debounceTimer);
46267
+ entry.debounceTimer = null;
46268
+ }
46269
+ if (entry.reapTimer != null) {
46270
+ clearTimer(entry.reapTimer);
46271
+ entry.reapTimer = null;
46272
+ }
46273
+ }
46274
+ function dropEntry(entry) {
46275
+ clearTimers(entry);
46276
+ byKey.delete(entry.statusKey);
46277
+ bySyntheticKey.delete(entry.syntheticTurnKey);
46278
+ }
46279
+ function emit(entry) {
46280
+ entry.debounceTimer = null;
46281
+ if (entry.consumed)
46282
+ return;
46283
+ if (deps.isTurnSettled?.(entry.statusKey)) {
46284
+ dropEntry(entry);
46285
+ return;
46286
+ }
46287
+ deps.startTypingLoop(entry.chatId, entry.threadId);
46288
+ entry.emitted = true;
46289
+ entry.reapTimer = setTimer(() => reap(entry), adoptTimeoutMs);
46290
+ Promise.resolve().then(() => deps.openCard(entry.chatId, entry.threadId)).then((messageId) => {
46291
+ if (messageId == null)
46292
+ return;
46293
+ if (entry.consumed) {
46294
+ deps.finalizeCard({
46295
+ turnKey: entry.syntheticTurnKey,
46296
+ chatId: entry.chatId,
46297
+ threadId: entry.threadId,
46298
+ activityMessageId: messageId,
46299
+ startedAt: entry.startedAt,
46300
+ pinned: entry.pinned
46301
+ });
46302
+ return;
46303
+ }
46304
+ entry.activityMessageId = messageId;
46305
+ const record = {
46306
+ turnKey: entry.syntheticTurnKey,
46307
+ chatId: entry.chatId,
46308
+ threadId: entry.threadId,
46309
+ activityMessageId: messageId,
46310
+ startedAt: entry.startedAt,
46311
+ pinned: entry.pinned
46312
+ };
46313
+ deps.writeCardRecord(record);
46314
+ }).catch((err) => {
46315
+ log(`handback-preturn-signal: openCard failed key=${entry.statusKey}: ` + `${err instanceof Error ? err.message : String(err)}
46316
+ `);
46317
+ });
46318
+ }
46319
+ function reap(entry) {
46320
+ entry.reapTimer = null;
46321
+ if (entry.consumed)
46322
+ return;
46323
+ entry.consumed = true;
46324
+ deps.stopTypingLoop(entry.chatId, entry.threadId);
46325
+ if (entry.activityMessageId != null) {
46326
+ const record = {
46327
+ turnKey: entry.syntheticTurnKey,
46328
+ chatId: entry.chatId,
46329
+ threadId: entry.threadId,
46330
+ activityMessageId: entry.activityMessageId,
46331
+ startedAt: entry.startedAt,
46332
+ pinned: entry.pinned
46333
+ };
46334
+ deps.clearCardRecord(entry.syntheticTurnKey, entry.activityMessageId);
46335
+ Promise.resolve(deps.finalizeCard(record)).catch((err) => {
46336
+ log(`handback-preturn-signal: orphan finalize failed key=${entry.statusKey}: ` + `${err instanceof Error ? err.message : String(err)}
46337
+ `);
46338
+ });
46339
+ }
46340
+ dropEntry(entry);
46341
+ }
46342
+ return {
46343
+ noteHandbackRelease(inbound) {
46344
+ if (!isHandbackInbound(inbound))
46345
+ return;
46346
+ const chatId = inbound.chatId;
46347
+ if (chatId == null || chatId === "")
46348
+ return;
46349
+ const threadId = inbound.threadId ?? null;
46350
+ const adoptTurnId = deps.deriveTurnId(chatId, threadId, inbound.messageId);
46351
+ if (adoptTurnId == null)
46352
+ return;
46353
+ const statusKey = deps.chatKey(chatId, threadId);
46354
+ if (byKey.has(statusKey))
46355
+ return;
46356
+ const startedAt = now();
46357
+ const syntheticTurnKey = `${PRETURN_TURNKEY_PREFIX}${statusKey}:${startedAt}`;
46358
+ const entry = {
46359
+ statusKey,
46360
+ chatId,
46361
+ threadId,
46362
+ adoptTurnId,
46363
+ syntheticTurnKey,
46364
+ startedAt,
46365
+ pinned: false,
46366
+ debounceTimer: null,
46367
+ reapTimer: null,
46368
+ activityMessageId: null,
46369
+ emitted: false,
46370
+ consumed: false
46371
+ };
46372
+ byKey.set(statusKey, entry);
46373
+ bySyntheticKey.set(syntheticTurnKey, statusKey);
46374
+ entry.debounceTimer = setTimer(() => emit(entry), debounceMs);
46375
+ },
46376
+ tryAdopt(turnId) {
46377
+ let entry;
46378
+ for (const e of byKey.values()) {
46379
+ if (e.adoptTurnId === turnId && !e.consumed) {
46380
+ entry = e;
46381
+ break;
46382
+ }
46383
+ }
46384
+ if (entry == null)
46385
+ return null;
46386
+ entry.consumed = true;
46387
+ clearTimers(entry);
46388
+ const adoption = {
46389
+ statusKey: entry.statusKey,
46390
+ chatId: entry.chatId,
46391
+ threadId: entry.threadId,
46392
+ activityMessageId: entry.activityMessageId,
46393
+ startedAt: entry.startedAt,
46394
+ pinned: entry.pinned
46395
+ };
46396
+ if (entry.activityMessageId != null) {
46397
+ deps.clearCardRecord(entry.syntheticTurnKey, entry.activityMessageId);
46398
+ deps.writeCardRecord({
46399
+ turnKey: entry.statusKey,
46400
+ chatId: entry.chatId,
46401
+ threadId: entry.threadId,
46402
+ activityMessageId: entry.activityMessageId,
46403
+ startedAt: entry.startedAt,
46404
+ pinned: entry.pinned
46405
+ });
46406
+ }
46407
+ dropEntry(entry);
46408
+ return adoption;
46409
+ },
46410
+ isPreTurnRecord(turnKey2) {
46411
+ return turnKey2.startsWith(PRETURN_TURNKEY_PREFIX);
46412
+ },
46413
+ handleReaped(turnKey2) {
46414
+ const statusKey = bySyntheticKey.get(turnKey2);
46415
+ if (statusKey == null)
46416
+ return;
46417
+ const entry = byKey.get(statusKey);
46418
+ if (entry == null)
46419
+ return;
46420
+ entry.consumed = true;
46421
+ deps.stopTypingLoop(entry.chatId, entry.threadId);
46422
+ dropEntry(entry);
46423
+ },
46424
+ pendingCount() {
46425
+ let n = 0;
46426
+ for (const e of byKey.values())
46427
+ if (!e.consumed)
46428
+ n++;
46429
+ return n;
46430
+ },
46431
+ stopAll() {
46432
+ for (const e of [...byKey.values()])
46433
+ clearTimers(e);
46434
+ byKey.clear();
46435
+ bySyntheticKey.clear();
46436
+ }
46437
+ };
46438
+ }
46439
+
46440
+ // gateway/derive-turn-id.ts
46441
+ function deriveTurnId(chatId, threadId, messageId) {
46442
+ if (messageId == null || messageId === "" || String(messageId) === "0")
46443
+ return null;
46444
+ return `${chatKey(chatId, threadId ?? null)}#${messageId}`;
46445
+ }
46446
+
45872
46447
  // typing-emitter.ts
45873
46448
  var TYPING_REFRESH_MS = 4000;
45874
46449
  var TYPING_FLOOR_MS = 3500;
@@ -56064,7 +56639,9 @@ var SEND_GATE_DEFAULTS = {
56064
56639
  perChatBurst: 3,
56065
56640
  perGroupPerMin: 18,
56066
56641
  perGroupBurst: 2,
56067
- editFloorMs: 1500
56642
+ editFloorMs: 1500,
56643
+ perMessageEditWindowMs: 300000,
56644
+ perMessageEditMaxPerWindow: 150
56068
56645
  };
56069
56646
  function createSendGate(config) {
56070
56647
  const enabled2 = config.enabled;
@@ -56076,6 +56653,8 @@ function createSendGate(config) {
56076
56653
  const perGroupPerMin = config.perGroupPerMin ?? SEND_GATE_DEFAULTS.perGroupPerMin;
56077
56654
  const perGroupBurst = config.perGroupBurst ?? SEND_GATE_DEFAULTS.perGroupBurst;
56078
56655
  const editFloorMs = config.editFloorMs ?? SEND_GATE_DEFAULTS.editFloorMs;
56656
+ const perMessageEditWindowMs = Math.max(1, Math.floor(config.perMessageEditWindowMs ?? SEND_GATE_DEFAULTS.perMessageEditWindowMs));
56657
+ const perMessageEditMaxPerWindow = Math.max(0, Math.floor(config.perMessageEditMaxPerWindow ?? SEND_GATE_DEFAULTS.perMessageEditMaxPerWindow));
56079
56658
  const messageStateTtlMs = config.messageStateTtlMs ?? 60000;
56080
56659
  const maxMessageStates = config.maxMessageStates ?? 5000;
56081
56660
  const usefulTtlMs = config.usefulTtlMs ?? 120000;
@@ -56091,7 +56670,8 @@ function createSendGate(config) {
56091
56670
  dropped: 0,
56092
56671
  shed: 0,
56093
56672
  expired: 0,
56094
- failedFast: 0
56673
+ failedFast: 0,
56674
+ budgetDeferred: 0
56095
56675
  };
56096
56676
  const bootStart = clock.now();
56097
56677
  const globalRamp = config.bootRamp ? {
@@ -56130,7 +56710,8 @@ function createSendGate(config) {
56130
56710
  lastHash: undefined,
56131
56711
  pending: null,
56132
56712
  running: false,
56133
- suppressedUntilMs: 0
56713
+ suppressedUntilMs: 0,
56714
+ editWindowTs: []
56134
56715
  };
56135
56716
  perMessage.set(key, state);
56136
56717
  }
@@ -56317,7 +56898,20 @@ function createSendGate(config) {
56317
56898
  try {
56318
56899
  while (state.pending) {
56319
56900
  const now = clock.now();
56320
- const readyAt = Math.max(state.lastSentMs + editFloorMs, state.suppressedUntilMs);
56901
+ let readyAt = Math.max(state.lastSentMs + editFloorMs, state.suppressedUntilMs);
56902
+ if (perMessageEditMaxPerWindow > 0 && state.pending.priorityClass === "cosmetic") {
56903
+ const windowStart = now - perMessageEditWindowMs;
56904
+ while (state.editWindowTs.length > 0 && state.editWindowTs[0] <= windowStart) {
56905
+ state.editWindowTs.shift();
56906
+ }
56907
+ if (state.editWindowTs.length >= perMessageEditMaxPerWindow) {
56908
+ const budgetReadyAt = state.editWindowTs[0] + perMessageEditWindowMs;
56909
+ if (budgetReadyAt > readyAt) {
56910
+ readyAt = budgetReadyAt;
56911
+ counters.budgetDeferred++;
56912
+ }
56913
+ }
56914
+ }
56321
56915
  const waitMs = readyAt - now;
56322
56916
  if (waitMs > 0) {
56323
56917
  await clock.sleep(waitMs);
@@ -56343,6 +56937,12 @@ function createSendGate(config) {
56343
56937
  await admit(bucketsFor(opts));
56344
56938
  }
56345
56939
  state.lastSentMs = clock.now();
56940
+ if (perMessageEditMaxPerWindow > 0 && p.priorityClass === "cosmetic") {
56941
+ state.editWindowTs.push(state.lastSentMs);
56942
+ const overflow = state.editWindowTs.length - (perMessageEditMaxPerWindow + 1);
56943
+ if (overflow > 0)
56944
+ state.editWindowTs.splice(0, overflow);
56945
+ }
56346
56946
  try {
56347
56947
  const res = await p.fn();
56348
56948
  state.lastHash = p.hash;
@@ -56530,6 +57130,12 @@ function sendGateConfigFromEnv(env = process.env) {
56530
57130
  const editFloorMs = parseNonNegativeInt(env.SWITCHROOM_TG_SEND_GATE_EDIT_FLOOR_MS);
56531
57131
  if (editFloorMs !== undefined)
56532
57132
  out.editFloorMs = editFloorMs;
57133
+ const perMsgWindowMs = parsePositiveInt(env.SWITCHROOM_TG_SEND_GATE_PER_MSG_EDIT_WINDOW_MS);
57134
+ if (perMsgWindowMs !== undefined)
57135
+ out.perMessageEditWindowMs = perMsgWindowMs;
57136
+ const perMsgMax = parseNonNegativeInt(env.SWITCHROOM_TG_SEND_GATE_PER_MSG_EDIT_MAX);
57137
+ if (perMsgMax !== undefined)
57138
+ out.perMessageEditMaxPerWindow = perMsgMax;
56533
57139
  const conservativeGlobal = parseBoolFlag(env.SWITCHROOM_TG_SEND_GATE_CONSERVATIVE_GLOBAL);
56534
57140
  if (conservativeGlobal !== undefined)
56535
57141
  out.conservativeGlobalFloodScope = conservativeGlobal;
@@ -67710,9 +68316,21 @@ function backOffOpenInline2(text4, cut) {
67710
68316
  }
67711
68317
 
67712
68318
  // rich-send.ts
68319
+ init_dollar_math_guard();
68320
+ init_emphasis_guard();
68321
+ init_line_start_guard();
68322
+ init_inline_pairs_guard();
67713
68323
  var import_grammy8 = __toESM(require_mod2(), 1);
68324
+ function guardAccidentalFormatting2(markdown) {
68325
+ let out = markdown;
68326
+ out = guardAccidentalEmphasis(out);
68327
+ out = guardAccidentalBlockConstructs(out);
68328
+ out = guardAccidentalInlinePairs(out);
68329
+ out = guardDollarMath(out);
68330
+ return out;
68331
+ }
67714
68332
  function richMessage2(markdown) {
67715
- return { markdown };
68333
+ return { markdown: guardAccidentalFormatting2(markdown) };
67716
68334
  }
67717
68335
 
67718
68336
  // gateway/redelivery-decision.ts
@@ -70565,6 +71183,15 @@ function parseModelCommand(text4) {
70565
71183
  function isModelCommandBusy(ctx) {
70566
71184
  return ctx.currentTurnActive || ctx.turnInFlight;
70567
71185
  }
71186
+ function resolveStaleAwareBusy(input) {
71187
+ const turnStale = input.currentTurnActive && input.turnAgeMs !== null && input.turnAgeMs > input.hardTtlMs;
71188
+ const approvalLive = input.oldestPendingApprovalAgeMs !== null && input.oldestPendingApprovalAgeMs <= input.hardTtlMs;
71189
+ return {
71190
+ currentTurnActive: input.currentTurnActive && !turnStale,
71191
+ turnInFlight: input.machineInTurn || approvalLive,
71192
+ clearStaleTurn: turnStale
71193
+ };
71194
+ }
70568
71195
  function planModelCommand(parsed, ctx) {
70569
71196
  if (parsed.kind === "show" && ctx.menuEnabled)
70570
71197
  return { kind: "menu" };
@@ -75187,10 +75814,34 @@ function validateMs365Preview(input) {
75187
75814
  out.sizeBytesBefore = o.sizeBytesBefore;
75188
75815
  if (typeof o.sizeBytesAfter === "number")
75189
75816
  out.sizeBytesAfter = o.sizeBytesAfter;
75817
+ if (typeof o.eventWhen === "string")
75818
+ out.eventWhen = o.eventWhen;
75819
+ const changes = sanitizeChanges(o.changes);
75820
+ if (changes)
75821
+ out.changes = changes;
75190
75822
  if (typeof o.agentRationale === "string")
75191
75823
  out.agentRationale = o.agentRationale;
75192
75824
  return out;
75193
75825
  }
75826
+ function sanitizeChanges(input) {
75827
+ if (!Array.isArray(input))
75828
+ return;
75829
+ const out = [];
75830
+ for (const raw of input) {
75831
+ if (!raw || typeof raw !== "object")
75832
+ continue;
75833
+ const c = raw;
75834
+ if (typeof c.field !== "string" || c.field.length === 0)
75835
+ continue;
75836
+ const entry = { field: c.field };
75837
+ if (typeof c.before === "string")
75838
+ entry.before = c.before;
75839
+ if (typeof c.after === "string")
75840
+ entry.after = c.after;
75841
+ out.push(entry);
75842
+ }
75843
+ return out.length > 0 ? out : undefined;
75844
+ }
75194
75845
  var DEFAULT_TTL_MS2 = 5 * 60 * 1000;
75195
75846
  var MAX_TTL_MS2 = 30 * 60 * 1000;
75196
75847
  var MIN_TTL_MS2 = 30 * 1000;
@@ -75215,6 +75866,9 @@ function buildMs365CardText(p) {
75215
75866
  lines.push(`ID: ${truncate3(p.itemId, 96)}`);
75216
75867
  }
75217
75868
  lines.push(`Account: ${truncate3(p.accountEmail, 96)}`);
75869
+ if (p.eventWhen) {
75870
+ lines.push(`When: ${truncate3(p.eventWhen, 96)}`);
75871
+ }
75218
75872
  if (typeof p.sizeBytesBefore === "number" || typeof p.sizeBytesAfter === "number") {
75219
75873
  const before = p.sizeBytesBefore ?? 0;
75220
75874
  const after = p.sizeBytesAfter ?? 0;
@@ -75225,19 +75879,29 @@ function buildMs365CardText(p) {
75225
75879
  if (p.deepLink) {
75226
75880
  lines.push(`Link: ${truncate3(p.deepLink, 256)}`);
75227
75881
  }
75882
+ if (p.changes && p.changes.length > 0) {
75883
+ lines.push("");
75884
+ lines.push("Changes:");
75885
+ for (const c of p.changes.slice(0, 8)) {
75886
+ const before = c.before !== undefined ? truncate3(c.before, 96) : "(none)";
75887
+ const after = c.after !== undefined ? truncate3(c.after, 96) : "(cleared)";
75888
+ lines.push(`\u2022 ${c.field}: ${before} \u2192 ${after}`);
75889
+ }
75890
+ }
75228
75891
  if (p.agentRationale) {
75229
75892
  lines.push("");
75230
75893
  lines.push(`\uD83D\uDCAC ${truncate3(p.agentRationale, 512)}`);
75231
75894
  }
75232
75895
  lines.push("");
75233
- lines.push("\u26a0\ufe0f Weak attestation (RFC \u00a78 v1): operator should click through to verify the actual change before approving. Structural diff coming v1.5.");
75896
+ lines.push(p.changes && p.changes.length > 0 ? "\u26a0\ufe0f Attestation (RFC \u00a78 v1.5): the diff above is derived from live Graph state + the mutation payload. Verify before approving." : "\u26a0\ufe0f Weak attestation (RFC \u00a78 v1): operator should click through to verify the actual change before approving. Structural diff coming v1.5.");
75234
75897
  return hardenCardBreaks(lines.join(`
75235
75898
  `));
75236
75899
  }
75237
75900
  function truncate3(s, n) {
75238
- if (s.length <= n)
75239
- return s;
75240
- return s.slice(0, n - 1) + "\u2026";
75901
+ const oneLine = s.replace(/[\r\n\t]+/g, " ");
75902
+ if (oneLine.length <= n)
75903
+ return oneLine;
75904
+ return oneLine.slice(0, n - 1) + "\u2026";
75241
75905
  }
75242
75906
  function humanBytes(bytes) {
75243
75907
  const abs = Math.abs(bytes);
@@ -77828,6 +78492,7 @@ ${result}
77828
78492
  meta: {
77829
78493
  source: "subagent_handback",
77830
78494
  outcome: opts.ctx.outcome,
78495
+ message_id: String(ts),
77831
78496
  ...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
77832
78497
  ...opts.ctx.jsonlAgentId ? { subagent_jsonl_id: opts.ctx.jsonlAgentId } : {}
77833
78498
  }
@@ -79666,6 +80331,7 @@ import {
79666
80331
  } from "node:fs";
79667
80332
  import { join as join39 } from "node:path";
79668
80333
  var TURN_ACTIVE_MARKER_FILE = "turn-active.json";
80334
+ var TURN_ACTIVE_HARD_TTL_MS = 10 * 60000;
79669
80335
  function touchTurnActiveMarker(stateDir) {
79670
80336
  const path2 = join39(stateDir, TURN_ACTIVE_MARKER_FILE);
79671
80337
  if (!existsSync34(path2))
@@ -83392,6 +84058,8 @@ import {
83392
84058
  } from "node:fs";
83393
84059
  import { join as join52 } from "node:path";
83394
84060
  var TURN_ACTIVE_MARKER_FILE2 = "turn-active.json";
84061
+ var TURN_ACTIVE_HARD_TTL_MS2 = 10 * 60000;
84062
+ var TURN_ACTIVE_IDLE_SWEEP_MS = 60000;
83395
84063
  function writeTurnActiveMarker(stateDir, marker) {
83396
84064
  try {
83397
84065
  mkdirSync37(stateDir, { recursive: true });
@@ -83458,12 +84126,15 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
83458
84126
  return null;
83459
84127
  }
83460
84128
  }
84129
+ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
84130
+ return markerAgeMs ?? now - turnStartedAt;
84131
+ }
83461
84132
 
83462
84133
  // ../src/build-info.ts
83463
- var VERSION = "0.18.25";
83464
- var COMMIT_SHA = "01803cff";
83465
- var COMMIT_DATE = "2026-07-15T16:26:26+10:00";
83466
- var LATEST_PR = 3254;
84134
+ var VERSION = "0.18.27";
84135
+ var COMMIT_SHA = "93871829";
84136
+ var COMMIT_DATE = "2026-07-16T01:31:20Z";
84137
+ var LATEST_PR = 3273;
83467
84138
  var COMMITS_AHEAD_OF_TAG = 0;
83468
84139
 
83469
84140
  // gateway/boot-version.ts
@@ -86143,9 +86814,43 @@ var FEED_LIVENESS_OPEN_MS = (() => {
86143
86814
  var POST_ANSWER_LIVENESS_STALE_MS = parsePostAnswerLivenessMs(process.env.SWITCHROOM_POST_ANSWER_LIVENESS_STALE_MS) || 30000;
86144
86815
  function turnInFlightForGate() {
86145
86816
  const hasPendingApproval = pendingPermissions.size > 0;
86817
+ return turnInFlightMachineOnly() || hasPendingApproval;
86818
+ }
86819
+ function turnInFlightMachineOnly() {
86146
86820
  if (!isDeliveryCutoverEnabled())
86147
- return claudeBusyKeys.size > 0 || hasPendingApproval;
86148
- return probeGateParity(isMachineInTurn(), claudeBusyKeys.size) || hasPendingApproval;
86821
+ return claudeBusyKeys.size > 0;
86822
+ return probeGateParity(isMachineInTurn(), claudeBusyKeys.size);
86823
+ }
86824
+ function liveTurnAgeMs(now) {
86825
+ if (currentTurn === null)
86826
+ return null;
86827
+ return effectiveTurnAgeMs(readTurnActiveMarkerAgeMs(STATE_DIR, now), currentTurn.startedAt, now);
86828
+ }
86829
+ function oldestPendingApprovalAgeMs(now) {
86830
+ let oldest = null;
86831
+ for (const p of pendingPermissions.values()) {
86832
+ const age = now - p.startedAt;
86833
+ if (oldest === null || age > oldest)
86834
+ oldest = age;
86835
+ }
86836
+ return oldest;
86837
+ }
86838
+ function resolveModelEffortBusy(now = Date.now()) {
86839
+ const turnAgeMs = liveTurnAgeMs(now);
86840
+ const resolved = resolveStaleAwareBusy({
86841
+ currentTurnActive: currentTurn !== null,
86842
+ turnAgeMs,
86843
+ machineInTurn: turnInFlightMachineOnly(),
86844
+ oldestPendingApprovalAgeMs: oldestPendingApprovalAgeMs(now),
86845
+ hardTtlMs: TURN_ACTIVE_HARD_TTL_MS2
86846
+ });
86847
+ if (resolved.clearStaleTurn && currentTurn !== null) {
86848
+ const ageSec = Math.round((turnAgeMs ?? 0) / 1000);
86849
+ process.stderr.write(`telegram gateway: [phantomturn] cleared stale currentTurn atom age=${ageSec}s ttl=${Math.round(TURN_ACTIVE_HARD_TTL_MS2 / 1000)}s for /model|/effort busy-check agent=${getMyAgentName()}
86850
+ `);
86851
+ clearAllCurrentTurns();
86852
+ }
86853
+ return { currentTurnActive: resolved.currentTurnActive, turnInFlight: resolved.turnInFlight };
86149
86854
  }
86150
86855
  function deliverResumeSyntheticOrBuffer(agent, inbound) {
86151
86856
  const decision = decideInboundDelivery({
@@ -86313,11 +87018,6 @@ function findTurnByQuotedMessageId(chatId, replyTo) {
86313
87018
  return null;
86314
87019
  return turn;
86315
87020
  }
86316
- function deriveTurnId(chatId, threadId, messageId) {
86317
- if (messageId == null || messageId === "" || String(messageId) === "0")
86318
- return null;
86319
- return `${chatKey2(chatId, threadId ?? null)}#${messageId}`;
86320
- }
86321
87021
  function findTurnByOriginId(originTurnId) {
86322
87022
  if (originTurnId == null || originTurnId === "")
86323
87023
  return null;
@@ -88143,8 +88843,8 @@ var pendingStateReaper = setInterval(() => {
88143
88843
  try {
88144
88844
  sweepStaleTurnActiveMarker(STATE_DIR, {
88145
88845
  turnInFlight: currentTurn?.registryKey != null,
88146
- idleSweepMs: 60000,
88147
- hardTtlMs: 600000,
88846
+ idleSweepMs: TURN_ACTIVE_IDLE_SWEEP_MS,
88847
+ hardTtlMs: TURN_ACTIVE_HARD_TTL_MS2,
88148
88848
  now,
88149
88849
  onRemove: ({ ageMs, reason, payload }) => {
88150
88850
  const agent = getMyAgentName();
@@ -88591,11 +89291,16 @@ async function runMidSessionCardReaper() {
88591
89291
  isLive: (record2) => topicKeys.has(record2.turnKey),
88592
89292
  ttlMs: MID_SESSION_CARD_REAPER_TTL_MS,
88593
89293
  now,
88594
- finalizeCard: (record2) => robustApiCall(() => lockedBot.api.editMessageText(record2.chatId, record2.activityMessageId, richMessage2(restartOrphanCardFinalizeText(record2.startedAt)), {}), {
88595
- chat_id: record2.chatId,
88596
- ...record2.threadId != null ? { threadId: record2.threadId } : {},
88597
- verb: "activity-card.mid-session-reap-finalize"
88598
- }),
89294
+ finalizeCard: (record2) => {
89295
+ if (handbackPreturnSignal.isPreTurnRecord(record2.turnKey)) {
89296
+ handbackPreturnSignal.handleReaped(record2.turnKey);
89297
+ }
89298
+ return robustApiCall(() => lockedBot.api.editMessageText(record2.chatId, record2.activityMessageId, richMessage2(restartOrphanCardFinalizeText(record2.startedAt)), {}), {
89299
+ chat_id: record2.chatId,
89300
+ ...record2.threadId != null ? { threadId: record2.threadId } : {},
89301
+ verb: "activity-card.mid-session-reap-finalize"
89302
+ });
89303
+ },
88599
89304
  unpinCard: async (record2) => {
88600
89305
  const pinKey = `fg:${record2.turnKey}`;
88601
89306
  if (statusPinState.has(pinKey)) {
@@ -89075,6 +89780,8 @@ var _deliveryMachineTick = setInterval(() => {
89075
89780
  }, DELIVERY_MACHINE_TICK_MS);
89076
89781
  _deliveryMachineTick.unref?.();
89077
89782
  function trackRedeliveredInbound(merged) {
89783
+ if (HANDBACK_PRETURN_ENABLED)
89784
+ handbackPreturnSignal.noteHandbackRelease(merged);
89078
89785
  if (!DELIVERY_CONFIRM_ENABLED)
89079
89786
  return;
89080
89787
  const isTrackableResume = isTrackableResumeSynthetic(merged.meta);
@@ -90754,6 +91461,7 @@ async function executeReply(args) {
90754
91461
  return { content: [{ type: "text", text: "sent (deduped \u2014 same content sent via earlier path)" }] };
90755
91462
  }
90756
91463
  }
91464
+ let supersedeFlushIds = [];
90757
91465
  {
90758
91466
  const replyThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
90759
91467
  const ownerTurn = resolveReplyOwnerTurn(turn, chat_id, args);
@@ -90762,9 +91470,9 @@ async function executeReply(args) {
90762
91470
  if (decision.supersede) {
90763
91471
  process.stderr.write(`telegram gateway: reply: superseding flushed turn message(s) chatId=${chat_id} ids=${JSON.stringify(decision.deleteMessageIds)}
90764
91472
  `);
90765
- for (const id of decision.deleteMessageIds) {
90766
- await swallowingApiCall(() => lockedBot.api.deleteMessage(chat_id, id), { chat_id, verb: "reply.supersedeFlushed" });
90767
- }
91473
+ supersedeFlushIds = decision.deleteMessageIds;
91474
+ if (ownerTurn != null)
91475
+ ownerTurn.answerDelivered = true;
90768
91476
  } else {
90769
91477
  const replySubstantive = isSubstantiveFinalReply({
90770
91478
  text: rawText,
@@ -90992,6 +91700,25 @@ ${url}`;
90992
91700
  });
90993
91701
  }
90994
91702
  }
91703
+ if (supersedeFlushIds.length > 0) {
91704
+ const correction = decideSupersedeCorrection({
91705
+ flushMessageIds: supersedeFlushIds,
91706
+ chunkCount: chunks.length,
91707
+ hasFiles: files.length > 0,
91708
+ suppressText,
91709
+ hasOpenPreview: previewMessageId != null
91710
+ });
91711
+ if (correction.mode === "edit-in-place") {
91712
+ previewMessageId = correction.editMessageId;
91713
+ reply_to = undefined;
91714
+ process.stderr.write(`telegram gateway: reply: superseding flushed message via edit-in-place chatId=${chat_id} id=${correction.editMessageId}
91715
+ `);
91716
+ } else {
91717
+ for (const id of correction.deleteMessageIds) {
91718
+ await swallowingApiCall(() => lockedBot.api.deleteMessage(chat_id, id), { chat_id, verb: "reply.supersedeFlushed" });
91719
+ }
91720
+ }
91721
+ }
90995
91722
  if (previewMessageId != null && reply_to != null && replyMode !== "off") {
90996
91723
  await deleteStalePreview(previewMessageId);
90997
91724
  previewMessageId = null;
@@ -91000,7 +91727,7 @@ ${url}`;
91000
91727
  let silentAnchorEditDone = false;
91001
91728
  {
91002
91729
  const turn2 = currentTurn;
91003
- if (turn2 != null && chunks.length === 1) {
91730
+ if (turn2 != null && chunks.length === 1 && supersedeFlushIds.length === 0) {
91004
91731
  const decision = decideSilentReplyAnchor({
91005
91732
  effectivelySilent: disableNotification,
91006
91733
  anchorMessageId: turn2.silentAnchorMessageId,
@@ -92815,6 +93542,61 @@ function clearActivitySummary(turn, finalHtmlOverride) {
92815
93542
  }
92816
93543
  });
92817
93544
  }
93545
+ var HANDBACK_PRETURN_ENABLED = !STATIC && process.env.SWITCHROOM_HANDBACK_PRETURN !== "0";
93546
+ var HANDBACK_PRETURN_HTML = "\uD83E\uDD1D Reading the worker\u2019s results\u2026";
93547
+ var HANDBACK_PRETURN_ORPHAN_HTML = "\uD83E\uDD1D A background worker finished, but the handback never started \u2014 it may need a nudge.";
93548
+ async function openHandbackPreTurnCard(chatId, threadId) {
93549
+ if (STATIC)
93550
+ return null;
93551
+ try {
93552
+ const sent = await robustApiCall(() => bot.api.sendRichMessage(chatId, richMessage2(HANDBACK_PRETURN_HTML), {
93553
+ ...threadId != null ? { message_thread_id: threadId } : {},
93554
+ disable_notification: true
93555
+ }), {
93556
+ chat_id: chatId,
93557
+ ...threadId != null ? { threadId } : {},
93558
+ verb: "handback-preturn.send"
93559
+ });
93560
+ return sent?.message_id ?? null;
93561
+ } catch (err) {
93562
+ process.stderr.write(`telegram gateway: handback pre-turn card send failed: ${err.message}
93563
+ `);
93564
+ return null;
93565
+ }
93566
+ }
93567
+ function finalizeHandbackPreTurnCard(record2) {
93568
+ return robustApiCall(() => bot.api.editMessageText(record2.chatId, record2.activityMessageId, richMessage2(HANDBACK_PRETURN_ORPHAN_HTML), {}), {
93569
+ chat_id: record2.chatId,
93570
+ ...record2.threadId != null ? { threadId: record2.threadId } : {},
93571
+ verb: "handback-preturn.orphan-finalize"
93572
+ }).then(() => {
93573
+ return;
93574
+ }).catch(() => {
93575
+ return;
93576
+ });
93577
+ }
93578
+ var handbackPreturnSignal = createHandbackPreturnSignal({
93579
+ chatKey: (chatId, threadId) => chatKey2(chatId, threadId),
93580
+ deriveTurnId: (chatId, threadId, messageId) => deriveTurnId(chatId, threadId, messageId),
93581
+ startTypingLoop: (chatId, threadId) => startTurnTypingLoop(chatId, threadId),
93582
+ stopTypingLoop: (chatId, threadId) => stopTurnTypingLoop(chatId, threadId),
93583
+ openCard: openHandbackPreTurnCard,
93584
+ finalizeCard: finalizeHandbackPreTurnCard,
93585
+ writeCardRecord: (record2) => {
93586
+ if (!activityCardPersistEnabled)
93587
+ return;
93588
+ writeActivityCardRecord(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs, record2);
93589
+ },
93590
+ clearCardRecord: (turnKey2, activityMessageId) => {
93591
+ if (!activityCardPersistEnabled)
93592
+ return;
93593
+ clearActivityCardRecord(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs, turnKey2, activityMessageId);
93594
+ },
93595
+ isTurnSettled: (key) => {
93596
+ const live = currentTurnMap.get(key);
93597
+ return live != null && (live.finalAnswerDelivered || live.endedAt != null);
93598
+ }
93599
+ });
92818
93600
  var memoryLegibilityStager = new MemoryLegibilityStager;
92819
93601
  function sendMemoryLegibilityLine(event, chatId, threadId) {
92820
93602
  const line = renderMemoryLegibilityLine(event);
@@ -92968,6 +93750,16 @@ function handleSessionEvent(ev) {
92968
93750
  emissionAuthority: new EmissionAuthority(statusKey(ev.chatId, enqThreadIdNum))
92969
93751
  };
92970
93752
  next.narrativeGate = makeNarrativeGate(next);
93753
+ if (HANDBACK_PRETURN_ENABLED) {
93754
+ const handbackAdoption = handbackPreturnSignal.tryAdopt(turnId);
93755
+ if (handbackAdoption != null) {
93756
+ if (handbackAdoption.activityMessageId != null) {
93757
+ next.activityMessageId = handbackAdoption.activityMessageId;
93758
+ next.activityEverOpened = true;
93759
+ }
93760
+ startTurnTypingLoop(ev.chatId, enqThreadIdNum ?? null);
93761
+ }
93762
+ }
92971
93763
  setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
92972
93764
  scheduleEarlyLivenessOpen(next);
92973
93765
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
@@ -96014,7 +96806,8 @@ bot.command("model", async (ctx) => {
96014
96806
  const parsed = parseModelCommand(text5) ?? { kind: "show" };
96015
96807
  const chatId = String(ctx.chat.id);
96016
96808
  const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
96017
- const busyNow = currentTurn !== null || turnInFlightForGate();
96809
+ const modelBusy = resolveModelEffortBusy();
96810
+ const busyNow = modelBusy.currentTurnActive || modelBusy.turnInFlight;
96018
96811
  process.stderr.write(modelCommandReceiptLine(getMyAgentName(), parsed, busyNow) + `
96019
96812
  `);
96020
96813
  if (HISTORY_ENABLED && ctx.message?.message_id != null) {
@@ -96035,8 +96828,8 @@ bot.command("model", async (ctx) => {
96035
96828
  }
96036
96829
  const deps = buildModelDeps({ chatId, threadId });
96037
96830
  const disposition = planModelCommand(parsed, {
96038
- currentTurnActive: currentTurn !== null,
96039
- turnInFlight: turnInFlightForGate(),
96831
+ currentTurnActive: modelBusy.currentTurnActive,
96832
+ turnInFlight: modelBusy.turnInFlight,
96040
96833
  menuEnabled: process.env.SWITCHROOM_MODEL_MENU !== "0"
96041
96834
  });
96042
96835
  if (disposition.kind === "menu") {
@@ -96107,7 +96900,8 @@ bot.command("effort", async (ctx) => {
96107
96900
  await switchroomReply(ctx, menu.text, { html: true, reply_markup: effortMenuReplyMarkup(menu) });
96108
96901
  return;
96109
96902
  }
96110
- if ((parsed.kind === "set" || parsed.kind === "default") && currentTurn !== null) {
96903
+ const effortBusy = resolveModelEffortBusy();
96904
+ if ((parsed.kind === "set" || parsed.kind === "default") && effortBusy.currentTurnActive) {
96111
96905
  const requestedLevel = parsed.kind === "set" ? parsed.level : "default";
96112
96906
  const chatId = String(ctx.chat.id);
96113
96907
  const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);