switchroom 0.18.21 → 0.18.23

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.
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.18.21", COMMIT_SHA = "c237ff59";
2123
+ var VERSION = "0.18.23", COMMIT_SHA = "a90cf518";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -23982,6 +23982,8 @@ function isOwnedStaleLink(target, poolDir) {
23982
23982
  return true;
23983
23983
  if (target.startsWith("/opt/skills/"))
23984
23984
  return true;
23985
+ if (target.includes("/switchroom-ai/skills/"))
23986
+ return true;
23985
23987
  return false;
23986
23988
  }
23987
23989
  function reconcileAgentDefaultSkills(agentDir, optOuts = {}, defaults = getBuiltinDefaultSkillEntries(), poolDir = getBundledSkillsPoolDir()) {
@@ -26605,7 +26605,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
26605
26605
  import { dirname as dirname4, join as join7 } from "node:path";
26606
26606
 
26607
26607
  // src/build-info.ts
26608
- var VERSION = "0.18.21";
26608
+ var VERSION = "0.18.23";
26609
26609
 
26610
26610
  // src/cli/resolve-version.ts
26611
26611
  function readPackageVersion() {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.18.21",
4
+ "version": "0.18.23",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -39283,6 +39283,92 @@ function normalizeForDedup(text) {
39283
39283
  return text.replace(/<\/?[a-zA-Z][^>]*>/g, "").replace(/&[a-zA-Z]+;|&#\d+;/g, " ").replace(/(\*\*|__|`)+/g, "").replace(/^[#>\-*+]\s+/gm, "").replace(/\s+/g, " ").trim().toLowerCase();
39284
39284
  }
39285
39285
 
39286
+ // flushed-turn-supersede.ts
39287
+ var DEFAULT_SUPERSEDE_TTL_MS = 60000;
39288
+ function decideSupersede(record, args) {
39289
+ const ttlMs = args.ttlMs ?? DEFAULT_SUPERSEDE_TTL_MS;
39290
+ if (record == null)
39291
+ return { supersede: false, deleteMessageIds: [], reason: "no-record" };
39292
+ if (args.now - record.ts > ttlMs) {
39293
+ return { supersede: false, deleteMessageIds: [], reason: "expired" };
39294
+ }
39295
+ const sameTurn = record.turnId != null ? record.turnId === args.liveTurnId : args.liveTurnId == null;
39296
+ if (!sameTurn) {
39297
+ return { supersede: false, deleteMessageIds: [], reason: "different-turn" };
39298
+ }
39299
+ return { supersede: true, deleteMessageIds: [...record.messageIds], reason: "supersede" };
39300
+ }
39301
+ var NULL_TURN_KEY = "<<null-turn>>";
39302
+ function turnKey(turnId) {
39303
+ return turnId == null ? NULL_TURN_KEY : turnId;
39304
+ }
39305
+
39306
+ class FlushedTurnSupersedeRegistry {
39307
+ entries = new Map;
39308
+ ttlMs;
39309
+ constructor(opts = {}) {
39310
+ this.ttlMs = opts.ttlMs ?? DEFAULT_SUPERSEDE_TTL_MS;
39311
+ }
39312
+ record(chatId, threadId, rec, now) {
39313
+ if (rec.messageIds.length === 0)
39314
+ return;
39315
+ this.sweep(now);
39316
+ const lane = makeKey2(chatId, threadId);
39317
+ let laneMap = this.entries.get(lane);
39318
+ if (laneMap == null) {
39319
+ laneMap = new Map;
39320
+ this.entries.set(lane, laneMap);
39321
+ }
39322
+ laneMap.set(turnKey(rec.turnId), {
39323
+ turnId: rec.turnId,
39324
+ messageIds: [...rec.messageIds],
39325
+ text: rec.text,
39326
+ ts: now
39327
+ });
39328
+ }
39329
+ peek(chatId, threadId, args) {
39330
+ const rec = this.entries.get(makeKey2(chatId, threadId))?.get(turnKey(args.liveTurnId));
39331
+ return decideSupersede(rec, { liveTurnId: args.liveTurnId, now: args.now, ttlMs: this.ttlMs });
39332
+ }
39333
+ take(chatId, threadId, args) {
39334
+ const lane = makeKey2(chatId, threadId);
39335
+ const decision = this.peek(chatId, threadId, args);
39336
+ if (decision.supersede) {
39337
+ const laneMap = this.entries.get(lane);
39338
+ laneMap?.delete(turnKey(args.liveTurnId));
39339
+ if (laneMap != null && laneMap.size === 0)
39340
+ this.entries.delete(lane);
39341
+ }
39342
+ return decision;
39343
+ }
39344
+ forget(chatId, threadId) {
39345
+ this.entries.delete(makeKey2(chatId, threadId));
39346
+ }
39347
+ clear() {
39348
+ this.entries.clear();
39349
+ }
39350
+ sweep(now) {
39351
+ for (const [lane, laneMap] of this.entries) {
39352
+ for (const [tk, rec] of laneMap) {
39353
+ if (now - rec.ts > this.ttlMs)
39354
+ laneMap.delete(tk);
39355
+ }
39356
+ if (laneMap.size === 0)
39357
+ this.entries.delete(lane);
39358
+ }
39359
+ }
39360
+ size(now) {
39361
+ this.sweep(now);
39362
+ let total = 0;
39363
+ for (const laneMap of this.entries.values())
39364
+ total += laneMap.size;
39365
+ return total;
39366
+ }
39367
+ }
39368
+ function makeKey2(chatId, threadId) {
39369
+ return threadId == null ? chatId : `${chatId}|${threadId}`;
39370
+ }
39371
+
39286
39372
  // gateway/inbound-coalesce.ts
39287
39373
  function createInboundCoalescer(opts) {
39288
39374
  const buffer = new Map;
@@ -40593,44 +40679,63 @@ function createWorkerActivityFeed(opts) {
40593
40679
  function removeWorker(g, agentId) {
40594
40680
  g.workers.delete(agentId);
40595
40681
  agentIndex.delete(agentId);
40596
- if (g.workers.size === 0)
40682
+ maybeDeleteGroup(g);
40683
+ }
40684
+ function maybeDeleteGroup(g) {
40685
+ if (g.workers.size === 0 && g.pendingFinalize.size === 0)
40597
40686
  groups.delete(g.feedKey);
40598
40687
  }
40688
+ function hasLiveWorker(g) {
40689
+ for (const w of g.workers.values())
40690
+ if (!w.finished)
40691
+ return true;
40692
+ return false;
40693
+ }
40599
40694
  function syncPin(g) {
40600
- const messageId = g.messageId != null && g.workers.size > 0 ? g.messageId : null;
40695
+ const messageId = g.messageId != null && hasLiveWorker(g) ? g.messageId : null;
40601
40696
  reconcilePinFn({ feedKey: g.feedKey, chatId: g.chatId, threadId: g.threadId, messageId });
40602
40697
  }
40603
40698
  async function doRender(g, opts2 = {}) {
40604
40699
  const now = nowFn();
40605
40700
  const isTerminal = opts2.terminalRecap != null;
40606
- const settleTerminal = () => {
40607
- g.pendingFinalize = null;
40608
- if (opts2.finishingAgentId != null)
40609
- removeWorker(g, opts2.finishingAgentId);
40701
+ const finishingAgentId = opts2.finishingAgentId;
40702
+ const stageRecap = () => {
40703
+ if (isTerminal && finishingAgentId != null && opts2.terminalRecap != null) {
40704
+ g.pendingFinalize.set(finishingAgentId, opts2.terminalRecap);
40705
+ }
40706
+ };
40707
+ const clearStaged = () => {
40708
+ if (finishingAgentId != null)
40709
+ g.pendingFinalize.delete(finishingAgentId);
40710
+ maybeDeleteGroup(g);
40610
40711
  syncPin(g);
40611
40712
  };
40713
+ if (isTerminal) {
40714
+ stageRecap();
40715
+ if (finishingAgentId != null && g.workers.has(finishingAgentId)) {
40716
+ removeWorker(g, finishingAgentId);
40717
+ }
40718
+ g.terminalPainted = !hasLiveWorker(g);
40719
+ syncPin(g);
40720
+ }
40612
40721
  if (now < g.cooldownUntil) {
40613
- if (isTerminal && opts2.terminalRecap != null)
40614
- g.pendingFinalize = opts2.terminalRecap;
40615
40722
  return;
40616
40723
  }
40617
40724
  if (parkIfFloodWindowOpen(g)) {
40618
- if (isTerminal && opts2.terminalRecap != null)
40619
- g.pendingFinalize = opts2.terminalRecap;
40620
40725
  return;
40621
40726
  }
40622
40727
  const body = renderGroupBody(g, now, opts2.terminalRecap ?? null, opts2.heartbeat ?? false);
40623
40728
  if (body == null) {
40624
40729
  if (isTerminal)
40625
- settleTerminal();
40730
+ clearStaged();
40626
40731
  return;
40627
40732
  }
40628
40733
  if (g.messageId == null) {
40629
- const maxElapsed = Math.max(0, ...runningRows(g).map((r) => liveElapsed(r, now)));
40630
40734
  if (isTerminal) {
40631
- settleTerminal();
40735
+ clearStaged();
40632
40736
  return;
40633
40737
  }
40738
+ const maxElapsed = Math.max(0, ...runningRows(g).map((r) => liveElapsed(r, now)));
40634
40739
  if (maxElapsed < firstPaintMin)
40635
40740
  return;
40636
40741
  try {
@@ -40643,6 +40748,7 @@ function createWorkerActivityFeed(opts) {
40643
40748
  g.messageId = sent.message_id;
40644
40749
  g.lastBody = body;
40645
40750
  g.lastEditAt = now;
40751
+ g.terminalPainted = false;
40646
40752
  syncPin(g);
40647
40753
  log(`worker-feed: paint feed=${g.feedKey} chat=${g.chatId} ` + `thread=${g.threadId ?? "-"} msgId=${g.messageId} workers=${g.workers.size} bytes=${body.length}`);
40648
40754
  } catch (err) {
@@ -40653,7 +40759,7 @@ function createWorkerActivityFeed(opts) {
40653
40759
  }
40654
40760
  if (body === g.lastBody) {
40655
40761
  if (isTerminal)
40656
- settleTerminal();
40762
+ clearStaged();
40657
40763
  return;
40658
40764
  }
40659
40765
  if (!opts2.force && now - g.lastEditAt < minEditInterval)
@@ -40662,45 +40768,39 @@ function createWorkerActivityFeed(opts) {
40662
40768
  const res = await opts.bot.editMessageText(g.chatId, g.messageId, body, sendOptsFor(g));
40663
40769
  if (isSendGateShed(res)) {
40664
40770
  parkIfFloodWindowOpen(g);
40665
- if (isTerminal && opts2.terminalRecap != null)
40666
- g.pendingFinalize = opts2.terminalRecap;
40667
40771
  return;
40668
40772
  }
40669
40773
  g.lastBody = body;
40670
40774
  g.lastEditAt = now;
40671
40775
  if (isTerminal) {
40672
- log(`worker-feed: finish feed=${g.feedKey} chat=${g.chatId} thread=${g.threadId ?? "-"} ` + `msgId=${g.messageId} agent=${opts2.finishingAgentId ?? "-"} ` + `state=${opts2.terminalRecap?.state ?? "done"} bytes=${body.length}`);
40776
+ 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}`);
40673
40777
  } else {
40674
40778
  log(`worker-feed: edit feed=${g.feedKey} chat=${g.chatId} ` + `thread=${g.threadId ?? "-"} msgId=${g.messageId} workers=${g.workers.size} bytes=${body.length}`);
40675
40779
  }
40676
40780
  if (isTerminal)
40677
- settleTerminal();
40781
+ clearStaged();
40678
40782
  } catch (err) {
40679
40783
  const outcome = classifyEditError(err);
40680
40784
  if (outcome === "rate_limited") {
40681
40785
  noteRateLimited(g, err, isTerminal ? "finish" : "edit");
40682
- if (isTerminal && opts2.terminalRecap != null)
40683
- g.pendingFinalize = opts2.terminalRecap;
40684
40786
  return;
40685
40787
  }
40686
40788
  if (outcome === "not_modified") {
40687
40789
  g.lastBody = body;
40688
40790
  g.lastEditAt = now;
40689
40791
  if (isTerminal)
40690
- settleTerminal();
40792
+ clearStaged();
40691
40793
  return;
40692
40794
  }
40693
40795
  if (outcome === "gone") {
40694
40796
  g.messageId = null;
40695
40797
  g.lastBody = null;
40696
40798
  if (isTerminal)
40697
- settleTerminal();
40799
+ clearStaged();
40698
40800
  else
40699
40801
  syncPin(g);
40700
40802
  return;
40701
40803
  }
40702
- if (isTerminal && opts2.terminalRecap != null)
40703
- g.pendingFinalize = opts2.terminalRecap;
40704
40804
  log(`worker-feed: edit transient error feed=${g.feedKey}: ${err.message}`);
40705
40805
  }
40706
40806
  }
@@ -40747,24 +40847,34 @@ function createWorkerActivityFeed(opts) {
40747
40847
  function heartbeatTick() {
40748
40848
  const now = nowFn();
40749
40849
  const staleAgentIds = [];
40850
+ const staleFinished = [];
40750
40851
  for (const g of groups.values()) {
40751
40852
  for (const row of g.workers.values()) {
40752
- if (!row.finished && now - row.lastUpdateAt >= staleWorkerTtlMs) {
40753
- staleAgentIds.push(row.agentId);
40853
+ if (now - row.lastUpdateAt >= staleWorkerTtlMs) {
40854
+ if (row.finished)
40855
+ staleFinished.push({ g, agentId: row.agentId });
40856
+ else
40857
+ staleAgentIds.push(row.agentId);
40754
40858
  }
40755
40859
  }
40756
40860
  }
40861
+ for (const { g, agentId } of staleFinished) {
40862
+ log(`worker-feed: TTL GC finished row agent=${agentId} feed=${g.feedKey} \u2014 reaping leaked finished row`);
40863
+ g.pendingFinalize.delete(agentId);
40864
+ removeWorker(g, agentId);
40865
+ syncPin(g);
40866
+ }
40757
40867
  for (const agentId of staleAgentIds) {
40758
40868
  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
40869
  terminateWorker(agentId);
40760
40870
  }
40761
40871
  for (const g of [...groups.values()]) {
40762
- if (g.pendingFinalize != null && now >= g.cooldownUntil) {
40763
- const recap = g.pendingFinalize;
40764
- const finishingAgentId = [...g.workers.values()].find((w) => w.finished)?.agentId;
40765
- g.chain = g.chain.then(() => doRender(g, { force: true, terminalRecap: recap, finishingAgentId })).catch((err) => {
40766
- log(`worker-feed: heartbeat finalize re-drive error feed=${g.feedKey}: ${err.message}`);
40767
- });
40872
+ if (g.pendingFinalize.size > 0 && now >= g.cooldownUntil) {
40873
+ for (const [agentId, recap] of [...g.pendingFinalize]) {
40874
+ g.chain = g.chain.then(() => doRender(g, { force: true, terminalRecap: recap, finishingAgentId: agentId })).catch((err) => {
40875
+ log(`worker-feed: heartbeat finalize re-drive error feed=${g.feedKey}: ${err.message}`);
40876
+ });
40877
+ }
40768
40878
  continue;
40769
40879
  }
40770
40880
  if (now < g.cooldownUntil)
@@ -40802,7 +40912,7 @@ function createWorkerActivityFeed(opts) {
40802
40912
  },
40803
40913
  hasRunningInFeed(feedKey) {
40804
40914
  const g = groups.get(feedKey);
40805
- return g != null && g.workers.size > 0;
40915
+ return g != null && hasLiveWorker(g);
40806
40916
  },
40807
40917
  get size() {
40808
40918
  let n = 0;
@@ -40831,10 +40941,18 @@ function createWorkerActivityFeed(opts) {
40831
40941
  cooldownUntil: 0,
40832
40942
  chain: Promise.resolve(),
40833
40943
  workers: new Map,
40834
- pendingFinalize: null
40944
+ pendingFinalize: new Map,
40945
+ terminalPainted: false
40835
40946
  };
40836
40947
  groups.set(feedKey, g);
40837
40948
  }
40949
+ if (!g.workers.has(agentId) && g.terminalPainted && !hasLiveWorker(g)) {
40950
+ g.messageId = null;
40951
+ g.lastBody = null;
40952
+ g.pendingFinalize.clear();
40953
+ g.terminalPainted = false;
40954
+ syncPin(g);
40955
+ }
40838
40956
  let row = g.workers.get(agentId);
40839
40957
  if (row == null) {
40840
40958
  row = {
@@ -62236,16 +62354,16 @@ function writeSilentEndState(args, deps) {
62236
62354
  `);
62237
62355
  }
62238
62356
  }
62239
- function clearSilentEndState(turnKey, deps) {
62357
+ function clearSilentEndState(turnKey2, deps) {
62240
62358
  const statePath = resolveStatePath2(deps);
62241
62359
  if (!existsSync14(statePath))
62242
62360
  return;
62243
62361
  try {
62244
62362
  const prev = JSON.parse(readFileSync16(statePath, "utf8"));
62245
- if (prev.turnKey != null && prev.turnKey !== turnKey)
62363
+ if (prev.turnKey != null && prev.turnKey !== turnKey2)
62246
62364
  return;
62247
62365
  unlinkSync9(statePath);
62248
- emitLog(deps, `silent-end: cleared state file turnKey=${turnKey}
62366
+ emitLog(deps, `silent-end: cleared state file turnKey=${turnKey2}
62249
62367
  `);
62250
62368
  } catch {}
62251
62369
  }
@@ -62407,6 +62525,23 @@ function endsWithSilentMarker(text4) {
62407
62525
  return false;
62408
62526
  return isSilentFlushMarker(lines[lines.length - 1]);
62409
62527
  }
62528
+ function selectFlushDeliveryText(blocks) {
62529
+ const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
62530
+ if (candidates.length === 0)
62531
+ return "";
62532
+ if (candidates.length === 1)
62533
+ return candidates[0];
62534
+ const answer = candidates[candidates.length - 1];
62535
+ const preceding = candidates.slice(0, -1);
62536
+ const allNarration = preceding.every(isNarrationBlock);
62537
+ return allNarration ? answer : candidates.join(`
62538
+
62539
+ `);
62540
+ }
62541
+ var NARRATION_OPENER = /^(let me\b|lemme\b|i'?ll\b|i will\b|i am going to\b|i'?m going to\b|i'?m about to\b|going to\b|first,?\s+(?:let me|i'?ll|i will)\b|now,?\s+(?:let me|i'?ll|i will)\b|next,?\s+(?:let me|i'?ll|i will)\b|let'?s\b)/i;
62542
+ function isNarrationBlock(block) {
62543
+ return NARRATION_OPENER.test(block.trimStart());
62544
+ }
62410
62545
  function decideTurnFlush(input) {
62411
62546
  const flushEnabled = input.flushEnabled !== false;
62412
62547
  if (!flushEnabled)
@@ -62426,7 +62561,7 @@ function decideTurnFlush(input) {
62426
62561
  return { kind: "skip", reason: "silent-marker" };
62427
62562
  if (endsWithSilentMarker(joined))
62428
62563
  return { kind: "skip", reason: "silent-marker" };
62429
- return { kind: "flush", text: joined };
62564
+ return { kind: "flush", text: selectFlushDeliveryText(input.capturedText) };
62430
62565
  }
62431
62566
 
62432
62567
  // answer-stream.ts
@@ -68030,6 +68165,23 @@ function endsWithSilentMarker2(text4) {
68030
68165
  return false;
68031
68166
  return isSilentFlushMarker2(lines[lines.length - 1]);
68032
68167
  }
68168
+ function selectFlushDeliveryText2(blocks) {
68169
+ const candidates = blocks.map((b) => b.trim()).filter((b) => b.length > 0);
68170
+ if (candidates.length === 0)
68171
+ return "";
68172
+ if (candidates.length === 1)
68173
+ return candidates[0];
68174
+ const answer = candidates[candidates.length - 1];
68175
+ const preceding = candidates.slice(0, -1);
68176
+ const allNarration = preceding.every(isNarrationBlock2);
68177
+ return allNarration ? answer : candidates.join(`
68178
+
68179
+ `);
68180
+ }
68181
+ var NARRATION_OPENER2 = /^(let me\b|lemme\b|i'?ll\b|i will\b|i am going to\b|i'?m going to\b|i'?m about to\b|going to\b|first,?\s+(?:let me|i'?ll|i will)\b|now,?\s+(?:let me|i'?ll|i will)\b|next,?\s+(?:let me|i'?ll|i will)\b|let'?s\b)/i;
68182
+ function isNarrationBlock2(block) {
68183
+ return NARRATION_OPENER2.test(block.trimStart());
68184
+ }
68033
68185
  function decideTurnFlush2(input) {
68034
68186
  const flushEnabled = input.flushEnabled !== false;
68035
68187
  if (!flushEnabled)
@@ -68049,7 +68201,7 @@ function decideTurnFlush2(input) {
68049
68201
  return { kind: "skip", reason: "silent-marker" };
68050
68202
  if (endsWithSilentMarker2(joined))
68051
68203
  return { kind: "skip", reason: "silent-marker" };
68052
- return { kind: "flush", text: joined };
68204
+ return { kind: "flush", text: selectFlushDeliveryText2(input.capturedText) };
68053
68205
  }
68054
68206
  function isTurnFlushSafetyEnabled(env = process.env) {
68055
68207
  const raw = env.SWITCHROOM_TG_TURN_FLUSH_SAFETY;
@@ -75253,11 +75405,11 @@ function writeActivityCardRecord(path2, fs2, record, log = (l) => process.stderr
75253
75405
  const others = current.filter((c) => c.turnKey !== record.turnKey);
75254
75406
  persistActivityCards(path2, fs2, [...others, record], log);
75255
75407
  }
75256
- function clearActivityCardRecord(path2, fs2, turnKey, activityMessageId, log = (l) => process.stderr.write(l)) {
75408
+ function clearActivityCardRecord(path2, fs2, turnKey2, activityMessageId, log = (l) => process.stderr.write(l)) {
75257
75409
  const current = loadActivityCards(path2, fs2);
75258
75410
  if (current.length === 0)
75259
75411
  return;
75260
- const next = current.filter((c) => c.turnKey !== turnKey || activityMessageId !== undefined && c.activityMessageId !== activityMessageId);
75412
+ const next = current.filter((c) => c.turnKey !== turnKey2 || activityMessageId !== undefined && c.activityMessageId !== activityMessageId);
75261
75413
  if (next.length === current.length)
75262
75414
  return;
75263
75415
  persistActivityCards(path2, fs2, next, log);
@@ -82758,10 +82910,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
82758
82910
  }
82759
82911
 
82760
82912
  // ../src/build-info.ts
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;
82913
+ var VERSION = "0.18.23";
82914
+ var COMMIT_SHA = "a90cf518";
82915
+ var COMMIT_DATE = "2026-07-14T09:32:46+10:00";
82916
+ var LATEST_PR = 3238;
82765
82917
  var COMMITS_AHEAD_OF_TAG = 0;
82766
82918
 
82767
82919
  // gateway/boot-version.ts
@@ -83853,8 +84005,8 @@ function recordTurnEnd(db2, args) {
83853
84005
  WHERE turn_key = ?
83854
84006
  `).run(now, args.endedVia, args.lastAssistantMsgId ?? null, args.lastAssistantDone !== undefined ? args.lastAssistantDone ? 1 : 0 : null, args.assistantReplyPreview ?? null, args.toolCallCount !== undefined ? args.toolCallCount : null, now, args.turnKey);
83855
84007
  }
83856
- function getTurnByKey(db2, turnKey) {
83857
- const row = db2.prepare(`SELECT * FROM turns WHERE turn_key = ?`).get(turnKey);
84008
+ function getTurnByKey(db2, turnKey2) {
84009
+ const row = db2.prepare(`SELECT * FROM turns WHERE turn_key = ?`).get(turnKey2);
83858
84010
  return row ? mapRow(row) : null;
83859
84011
  }
83860
84012
  function markOrphanedWithTimeoutClassification(db2, opts) {
@@ -83912,13 +84064,13 @@ var INTERRUPTED_VIA = new Set([
83912
84064
  "timeout",
83913
84065
  "unknown"
83914
84066
  ]);
83915
- function markTurnResumed(db2, turnKey, now = Date.now()) {
84067
+ function markTurnResumed(db2, turnKey2, now = Date.now()) {
83916
84068
  db2.prepare(`
83917
84069
  UPDATE turns
83918
84070
  SET resumed_at = ?,
83919
84071
  updated_at = ?
83920
84072
  WHERE turn_key = ? AND resumed_at IS NULL
83921
- `).run(now, now, turnKey);
84073
+ `).run(now, now, turnKey2);
83922
84074
  }
83923
84075
  function findLatestTurnIfInterrupted(db2) {
83924
84076
  const row = db2.prepare(`
@@ -85275,6 +85427,7 @@ var deferredDoneReactions = new DeferredDoneReactions({
85275
85427
  purge: (key) => purgeReactionTracking(key)
85276
85428
  });
85277
85429
  var outboundDedup = new OutboundDedupCache;
85430
+ var flushedTurnSupersede = new FlushedTurnSupersedeRegistry;
85278
85431
  var chatAvailableReactions = new Map;
85279
85432
  var chatProbesInFlight = new Set;
85280
85433
  var activeTurnStartedAt = new Map;
@@ -86956,10 +87109,10 @@ function summariseMarkerPayload(payload) {
86956
87109
  return "payload=missing";
86957
87110
  try {
86958
87111
  const parsed = JSON.parse(payload);
86959
- const turnKey = typeof parsed.turnKey === "string" ? parsed.turnKey : "unknown";
87112
+ const turnKey2 = typeof parsed.turnKey === "string" ? parsed.turnKey : "unknown";
86960
87113
  const chatId = typeof parsed.chatId === "string" ? parsed.chatId : "unknown";
86961
87114
  const startedAt = typeof parsed.startedAt === "number" ? parsed.startedAt : 0;
86962
- return `turnKey=${turnKey} chat=${chatId} started=${new Date(startedAt).toISOString()}`;
87115
+ return `turnKey=${turnKey2} chat=${chatId} started=${new Date(startedAt).toISOString()}`;
86963
87116
  } catch {
86964
87117
  return "payload=unparseable";
86965
87118
  }
@@ -89889,6 +90042,18 @@ async function executeReply(args) {
89889
90042
  return { content: [{ type: "text", text: "sent (deduped \u2014 same content sent via earlier path)" }] };
89890
90043
  }
89891
90044
  }
90045
+ {
90046
+ const replyThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
90047
+ const resolvedTurnId = turn?.turnId ?? findTurnByOriginId(args.origin_turn_id)?.turnId ?? null;
90048
+ const decision = flushedTurnSupersede.take(chat_id, replyThreadId, { liveTurnId: resolvedTurnId, now: Date.now() });
90049
+ if (decision.supersede) {
90050
+ process.stderr.write(`telegram gateway: reply: superseding flushed turn message(s) chatId=${chat_id} ids=${JSON.stringify(decision.deleteMessageIds)}
90051
+ `);
90052
+ for (const id of decision.deleteMessageIds) {
90053
+ await swallowingApiCall(() => lockedBot.api.deleteMessage(chat_id, id), { chat_id, verb: "reply.supersedeFlushed" });
90054
+ }
90055
+ }
90056
+ }
89892
90057
  const files = args.files ?? [];
89893
90058
  const quoteOptIn = args.quote !== false;
89894
90059
  let reply_to = args.reply_to != null ? Number(args.reply_to) : undefined;
@@ -91501,8 +91666,8 @@ function resetOrphanedReplyTimeout() {
91501
91666
  replyCalled: t.replyCalled,
91502
91667
  progressCardActive: false
91503
91668
  })) {
91504
- const turnKey = statusKey(t.sessionChatId, t.sessionThreadId);
91505
- const working = isLegitimatelyWorking(turnKey);
91669
+ const turnKey2 = statusKey(t.sessionChatId, t.sessionThreadId);
91670
+ const working = isLegitimatelyWorking(turnKey2);
91506
91671
  const humanWaiting = (() => {
91507
91672
  for (const entry of pendingAskUser.values()) {
91508
91673
  if (entry.chatId === t.sessionChatId)
@@ -91520,7 +91685,7 @@ function resetOrphanedReplyTimeout() {
91520
91685
  maxRearms: ORPHANED_REPLY_MAX_REARMS
91521
91686
  });
91522
91687
  if (decision.rearm) {
91523
- process.stderr.write(`telegram gateway: orphaned-reply fuse expired \u2014 re-arming` + ` (rearm ${t.liveness.orphanedReplyRearmCount}/${ORPHANED_REPLY_MAX_REARMS}, in_flight=${toolFlightTracker.inFlightCount()}, human_wait=${humanWaiting}, recently_streaming=${recentlyStreaming}, bg_work=${hasPendingAsyncDispatch(turnKey)})
91688
+ process.stderr.write(`telegram gateway: orphaned-reply fuse expired \u2014 re-arming` + ` (rearm ${t.liveness.orphanedReplyRearmCount}/${ORPHANED_REPLY_MAX_REARMS}, in_flight=${toolFlightTracker.inFlightCount()}, human_wait=${humanWaiting}, recently_streaming=${recentlyStreaming}, bg_work=${hasPendingAsyncDispatch(turnKey2)})
91524
91689
  `);
91525
91690
  resetOrphanedReplyTimeout();
91526
91691
  return;
@@ -92084,23 +92249,23 @@ function handleSessionEvent(ev) {
92084
92249
  clearSilentEndState(statusKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null));
92085
92250
  if (turnsDb != null) {
92086
92251
  const evThreadIdNum = ev.threadId != null ? Number(ev.threadId) : null;
92087
- const turnKey = chatKeyWithSuffix2(ev.chatId, evThreadIdNum, String(startedAt));
92088
- next.registryKey = turnKey;
92252
+ const turnKey2 = chatKeyWithSuffix2(ev.chatId, evThreadIdNum, String(startedAt));
92253
+ next.registryKey = turnKey2;
92089
92254
  const userPromptPreview = extractUserPromptPreview(ev.rawContent);
92090
92255
  try {
92091
92256
  recordTurnStart(turnsDb, {
92092
- turnKey,
92257
+ turnKey: turnKey2,
92093
92258
  chatId: String(ev.chatId),
92094
92259
  threadId: ev.threadId != null ? String(ev.threadId) : null,
92095
92260
  lastUserMsgId: ev.messageId != null ? String(ev.messageId) : null,
92096
92261
  userPromptPreview
92097
92262
  });
92098
92263
  } catch (err) {
92099
- process.stderr.write(`telegram gateway: recordTurnStart failed turnKey=${turnKey}: ${err.message}
92264
+ process.stderr.write(`telegram gateway: recordTurnStart failed turnKey=${turnKey2}: ${err.message}
92100
92265
  `);
92101
92266
  }
92102
92267
  writeTurnActiveMarker(STATE_DIR, {
92103
- turnKey,
92268
+ turnKey: turnKey2,
92104
92269
  chatId: String(ev.chatId),
92105
92270
  threadId: ev.threadId != null ? String(ev.threadId) : null,
92106
92271
  startedAt
@@ -92606,6 +92771,9 @@ function handleSessionEvent(ev) {
92606
92771
  } catch {}
92607
92772
  }
92608
92773
  outboundDedup.record(backstopChatId, backstopThreadId, capturedText, Date.now(), currentTurn?.registryKey ?? null);
92774
+ if (sentIds.length > 0) {
92775
+ flushedTurnSupersede.record(backstopChatId, backstopThreadId, { turnId: turn.turnId, messageIds: sentIds, text: capturedText }, Date.now());
92776
+ }
92609
92777
  if (backstopCtrl)
92610
92778
  backstopCtrl.finalize("done");
92611
92779
  if (backstopCardTurnKey != null) {