switchroom 0.19.40 → 0.19.41

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.19.40", COMMIT_SHA = "89286b22";
2123
+ var VERSION = "0.19.41", COMMIT_SHA = "e77ee9c0";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -70125,6 +70125,9 @@ var init_server4 = __esm(() => {
70125
70125
  ];
70126
70126
  });
70127
70127
 
70128
+ // telegram-plugin/gateway/turn-record-status.ts
70129
+ var ROUTE_FIELD_SHIP_TS = 1785456000;
70130
+
70128
70131
  // src/fleet-health/detect.ts
70129
70132
  function parseTurns(text) {
70130
70133
  const out = [];
@@ -70160,12 +70163,14 @@ function isoFromTs(ts) {
70160
70163
  function detectTurnFindings(agent, turns, opts = {}) {
70161
70164
  const findings = [];
70162
70165
  const silentNoopFloorTs = opts.silentNoopFloorTs ?? SILENT_NOOP_FLOOR_TS;
70166
+ const routeFieldShipTs = opts.routeFieldShipTs ?? ROUTE_FIELD_SHIP_TS;
70163
70167
  for (const t of ownedTurns(agent, turns)) {
70164
70168
  const tid = typeof t.turn_id === "string" ? t.turn_id : "?";
70165
70169
  const st = t.status;
70166
70170
  const tl = typeof t.tools === "number" ? t.tools : 0;
70167
70171
  const dur = typeof t.duration_ms === "number" ? t.duration_ms : 0;
70168
70172
  const synthetic = tid.includes("synthetic-");
70173
+ const route = typeof t.route === "string" ? t.route : undefined;
70169
70174
  const ts = isoFromTs(t.ts);
70170
70175
  if (st === "send_failed") {
70171
70176
  findings.push({
@@ -70194,13 +70199,33 @@ function detectTurnFindings(agent, turns, opts = {}) {
70194
70199
  });
70195
70200
  }
70196
70201
  if (st === "complete" && tl === 0 && !synthetic && t.ts != null && t.ts >= silentNoopFloorTs) {
70197
- findings.push({
70198
- signal: "silent-no-op-candidate",
70199
- agent,
70200
- turn_id: tid,
70201
- log_pointer: `turns.jsonl:${tid} tools=0`,
70202
- ts
70203
- });
70202
+ if (route === "flush") {
70203
+ findings.push({
70204
+ signal: "flush-recovered-turn",
70205
+ agent,
70206
+ turn_id: tid,
70207
+ log_pointer: `turns.jsonl:${tid} tools=0 route=flush`,
70208
+ ts
70209
+ });
70210
+ } else if (route === "reply" || route === "stream") {} else if (route === "none") {
70211
+ findings.push({
70212
+ signal: "silent-no-op-candidate",
70213
+ agent,
70214
+ turn_id: tid,
70215
+ log_pointer: `turns.jsonl:${tid} tools=0 route=none`,
70216
+ ts
70217
+ });
70218
+ } else {
70219
+ if (t.ts >= routeFieldShipTs) {
70220
+ findings.push({
70221
+ signal: "silent-no-op-candidate",
70222
+ agent,
70223
+ turn_id: tid,
70224
+ log_pointer: `turns.jsonl:${tid} tools=0`,
70225
+ ts
70226
+ });
70227
+ }
70228
+ }
70204
70229
  }
70205
70230
  }
70206
70231
  return findings;
@@ -70314,6 +70339,12 @@ var init_mapping = __esm(() => {
70314
70339
  job_spec: "know-what-my-agent-is-doing",
70315
70340
  signature: "silent-no-op:completed-zero-tools"
70316
70341
  },
70342
+ "flush-recovered-turn": {
70343
+ failure_mode: "drift",
70344
+ severity: 1,
70345
+ job_spec: "know-what-my-agent-is-doing",
70346
+ signature: "flush-recovered:completed-zero-tools-route-flush"
70347
+ },
70317
70348
  "duplicate-delivery-represent": {
70318
70349
  failure_mode: "duplicate",
70319
70350
  severity: 2,
@@ -21010,6 +21010,7 @@ function parsePendingRolloutMarker(raw) {
21010
21010
  if (o.allow_downgrade !== undefined && typeof o.allow_downgrade !== "boolean") {
21011
21011
  return null;
21012
21012
  }
21013
+ const narrationIdValid = typeof o.narration_message_id === "number" && Number.isInteger(o.narration_message_id) && o.narration_message_id > 0;
21013
21014
  return {
21014
21015
  v: 1,
21015
21016
  request_id: o.request_id,
@@ -21017,6 +21018,7 @@ function parsePendingRolloutMarker(raw) {
21017
21018
  ...o.agents !== undefined ? { agents: o.agents } : {},
21018
21019
  ...o.skip_web !== undefined ? { skip_web: o.skip_web } : {},
21019
21020
  ...o.allow_downgrade !== undefined ? { allow_downgrade: o.allow_downgrade } : {},
21021
+ ...narrationIdValid ? { narration_message_id: o.narration_message_id } : {},
21020
21022
  caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
21021
21023
  created_at: o.created_at,
21022
21024
  prior_hostd_version: o.prior_hostd_version
@@ -21320,7 +21322,7 @@ function allocateAgentUid(name) {
21320
21322
  }
21321
21323
 
21322
21324
  // src/build-info.ts
21323
- var VERSION = "0.19.40";
21325
+ var VERSION = "0.19.41";
21324
21326
 
21325
21327
  // src/setup/hindsight-recall-tunables.ts
21326
21328
  var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
@@ -30786,6 +30788,27 @@ class HostdServer {
30786
30788
  })
30787
30789
  };
30788
30790
  }
30791
+ persistNarrationMessageId(requestId, messageId) {
30792
+ try {
30793
+ if (!Number.isInteger(messageId) || messageId <= 0)
30794
+ return;
30795
+ const markerPath = join13(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
30796
+ if (!existsSync15(markerPath))
30797
+ return;
30798
+ const marker = parsePendingRolloutMarker(readFileSync11(markerPath, "utf8"));
30799
+ if (!marker)
30800
+ return;
30801
+ if (marker.request_id !== requestId)
30802
+ return;
30803
+ const updated = {
30804
+ ...marker,
30805
+ narration_message_id: messageId
30806
+ };
30807
+ const tmp = `${markerPath}.tmp`;
30808
+ writeFileSync6(tmp, encodePendingRolloutMarker(updated), { mode: 384 });
30809
+ renameSync5(tmp, markerPath);
30810
+ } catch {}
30811
+ }
30789
30812
  async resumePendingSelfBumpRollout() {
30790
30813
  const markerPath = join13(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
30791
30814
  if (!existsSync15(markerPath))
@@ -30854,6 +30877,9 @@ class HostdServer {
30854
30877
  ...marker.skip_web ? { skip_web: true } : {},
30855
30878
  ...marker.allow_downgrade ? { allow_downgrade: true } : {}
30856
30879
  }, marker.request_id, caller, Date.now());
30880
+ if (marker.narration_message_id !== undefined && caller.kind === "agent") {
30881
+ this.rolloutNarrator?.seedPostedMessage?.(marker.request_id, caller.name, marker.narration_message_id);
30882
+ }
30857
30883
  this.onRolloutPhase(entry, { phase: "self-bump-done", target: marker.pin });
30858
30884
  }
30859
30885
  async handleAgentStart(req, started) {
@@ -32672,28 +32698,49 @@ class LogTailRolloutNarrator {
32672
32698
  }
32673
32699
  }
32674
32700
  }
32701
+ freshState(agentName, target) {
32702
+ return {
32703
+ agentName,
32704
+ lastAppliedSeq: -1,
32705
+ messageId: null,
32706
+ posting: false,
32707
+ postAttempts: 0,
32708
+ postFailed: false,
32709
+ render: { target },
32710
+ agents: [],
32711
+ singletons: initialSingletons(),
32712
+ frozen: false,
32713
+ timer: null,
32714
+ pendingEditAfterPost: false
32715
+ };
32716
+ }
32675
32717
  ensureState(entry) {
32676
32718
  let st = this.states.get(entry.request_id);
32677
32719
  if (!st) {
32678
32720
  const agentName = entry.caller.kind === "agent" ? entry.caller.name : "";
32679
- st = {
32680
- agentName,
32681
- lastAppliedSeq: -1,
32682
- messageId: null,
32683
- posting: false,
32684
- postAttempts: 0,
32685
- postFailed: false,
32686
- render: { target: entry.pin ?? "" },
32687
- agents: [],
32688
- singletons: initialSingletons(),
32689
- frozen: false,
32690
- timer: null,
32691
- pendingEditAfterPost: false
32692
- };
32721
+ st = this.freshState(agentName, entry.pin ?? "");
32693
32722
  this.states.set(entry.request_id, st);
32694
32723
  }
32695
32724
  return st;
32696
32725
  }
32726
+ seedPostedMessage(requestId, agentName, messageId) {
32727
+ const existing = this.states.get(requestId);
32728
+ if (existing) {
32729
+ if (existing.messageId === null) {
32730
+ existing.messageId = messageId;
32731
+ existing.posting = false;
32732
+ existing.postFailed = false;
32733
+ if (existing.postAttempts < 1)
32734
+ existing.postAttempts = 1;
32735
+ existing.pendingEditAfterPost = false;
32736
+ }
32737
+ return;
32738
+ }
32739
+ const st = this.freshState(agentName, "");
32740
+ st.messageId = messageId;
32741
+ st.postAttempts = 1;
32742
+ this.states.set(requestId, st);
32743
+ }
32697
32744
  scheduleRenderOrPost(requestId) {
32698
32745
  const st = this.states.get(requestId);
32699
32746
  if (!st)
@@ -32734,6 +32781,11 @@ class LogTailRolloutNarrator {
32734
32781
  }
32735
32782
  st.messageId = mid;
32736
32783
  st.postFailed = false;
32784
+ try {
32785
+ this.opts.onMessageId?.(requestId, mid);
32786
+ } catch (e) {
32787
+ this.opts.log?.(`onMessageId sink threw (non-fatal): ${e.message}`);
32788
+ }
32737
32789
  if (st.pendingEditAfterPost) {
32738
32790
  st.pendingEditAfterPost = false;
32739
32791
  this.flush(requestId, false);
@@ -33378,8 +33430,11 @@ async function main() {
33378
33430
  resolveGatewaySocket,
33379
33431
  log: (m) => process.stderr.write(`hostd: rollout-narration — ${m}
33380
33432
  `)
33381
- }), { log: (m) => process.stderr.write(`hostd: rollout-narration — ${m}
33382
- `) });
33433
+ }), {
33434
+ log: (m) => process.stderr.write(`hostd: rollout-narration — ${m}
33435
+ `),
33436
+ onMessageId: (rid, mid) => server?.persistNarrationMessageId(rid, mid)
33437
+ });
33383
33438
  const server = new HostdServer({
33384
33439
  homeDir: homedir8(),
33385
33440
  agentUids,
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.19.40",
4
+ "version": "0.19.41",
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": {
@@ -268,7 +268,7 @@ const TOOL_SCHEMAS = [
268
268
  {
269
269
  name: 'send_checklist',
270
270
  description:
271
- 'Send a native Telegram checklist (interactive task list) to a chat. Users can tick tasks directly in the Telegram app. Returns the message_id of the created checklist. The bot is notified when tasks are ticked these arrive as channel events with kind="checklist_task_changed". Limit: 30 tasks per checklist.',
271
+ 'Send a checklist (task list) to a chat. Native interactive Telegram checklists require a Business-account connection, which most deployments do not have — in that normal case the checklist is sent as a formatted TEXT message (bold title + ✅/⬜ task lines) and the result carries degraded:"text": tasks are NOT tappable, and you tick them yourself via update_checklist (task ids are 1..N in send order). With a Business connection configured, the checklist is sent natively and tick events arrive as channel events with kind="checklist_task_changed". Returns JSON with message_id and mode. Limit: 30 tasks.',
272
272
  inputSchema: {
273
273
  type: 'object',
274
274
  properties: {
@@ -357,7 +357,7 @@ const TOOL_SCHEMAS = [
357
357
  {
358
358
  name: 'update_checklist',
359
359
  description:
360
- 'Patch an existing native Telegram checklist. Supports updating the title, adding new tasks, removing tasks, or marking tasks done/undone. Tasks with an id target existing items; tasks without an id are appended. Preserves existing task ids across edits.',
360
+ 'Patch a checklist previously sent with send_checklist: update the title, append tasks, or mark tasks done/undone. Tasks with an id target existing items (ids are 1..N in send order for text-mode checklists); tasks without an id are appended. Removal is not supported. The edit re-renders in the mode the checklist was sent in (text for most deployments). Returns JSON { ok, ... }; after a gateway restart the stored task state may be gone — then it returns ok:false with reason "unknown_checklist" unless you pass a full replacement (title + every task\'s text).',
361
361
  inputSchema: {
362
362
  type: 'object',
363
363
  properties: {
@@ -25241,7 +25241,7 @@ var TOOL_SCHEMAS = [
25241
25241
  },
25242
25242
  {
25243
25243
  name: "send_checklist",
25244
- description: 'Send a native Telegram checklist (interactive task list) to a chat. Users can tick tasks directly in the Telegram app. Returns the message_id of the created checklist. The bot is notified when tasks are ticked \u2014 these arrive as channel events with kind="checklist_task_changed". Limit: 30 tasks per checklist.',
25244
+ description: 'Send a checklist (task list) to a chat. Native interactive Telegram checklists require a Business-account connection, which most deployments do not have \u2014 in that normal case the checklist is sent as a formatted TEXT message (bold title + \u2705/\u2B1C task lines) and the result carries degraded:"text": tasks are NOT tappable, and you tick them yourself via update_checklist (task ids are 1..N in send order). With a Business connection configured, the checklist is sent natively and tick events arrive as channel events with kind="checklist_task_changed". Returns JSON with message_id and mode. Limit: 30 tasks.',
25245
25245
  inputSchema: {
25246
25246
  type: "object",
25247
25247
  properties: {
@@ -25326,7 +25326,7 @@ var TOOL_SCHEMAS = [
25326
25326
  },
25327
25327
  {
25328
25328
  name: "update_checklist",
25329
- description: "Patch an existing native Telegram checklist. Supports updating the title, adding new tasks, removing tasks, or marking tasks done/undone. Tasks with an id target existing items; tasks without an id are appended. Preserves existing task ids across edits.",
25329
+ description: `Patch a checklist previously sent with send_checklist: update the title, append tasks, or mark tasks done/undone. Tasks with an id target existing items (ids are 1..N in send order for text-mode checklists); tasks without an id are appended. Removal is not supported. The edit re-renders in the mode the checklist was sent in (text for most deployments). Returns JSON { ok, ... }; after a gateway restart the stored task state may be gone \u2014 then it returns ok:false with reason "unknown_checklist" unless you pass a full replacement (title + every task's text).`,
25330
25330
  inputSchema: {
25331
25331
  type: "object",
25332
25332
  properties: {
@@ -39504,6 +39504,178 @@ function redactChecklistFields(title, tasks, redact) {
39504
39504
  };
39505
39505
  }
39506
39506
 
39507
+ // gateway/checklist-fallback.ts
39508
+ var CHECKLIST_MAX_TASKS = 30;
39509
+ function buildChecklistTasks(tasks) {
39510
+ if (tasks.length > CHECKLIST_MAX_TASKS) {
39511
+ throw new Error(`checklist exceeds ${CHECKLIST_MAX_TASKS}-task limit (got ${tasks.length})`);
39512
+ }
39513
+ return tasks.map((t, i) => ({ id: i + 1, text: t.text, done: t.done === true }));
39514
+ }
39515
+ function renderChecklistText(cl, opts) {
39516
+ const header = opts?.literal ? cl.title : `**${cl.title}**`;
39517
+ const lines = cl.tasks.map((t) => `${t.done ? "\u2705" : "\u2b1c"} ${t.text}`);
39518
+ return [header, ...lines].join(`
39519
+ `);
39520
+ }
39521
+ function applyChecklistPatch(cl, patch) {
39522
+ const tasks = cl.tasks.map((t) => ({ ...t }));
39523
+ for (const p of patch.tasks ?? []) {
39524
+ const pid = p.id != null ? Number(p.id) : undefined;
39525
+ const existing = pid != null ? tasks.find((t) => t.id === pid) : undefined;
39526
+ if (existing) {
39527
+ if (p.text != null)
39528
+ existing.text = p.text;
39529
+ if (p.done != null)
39530
+ existing.done = p.done;
39531
+ } else {
39532
+ const nextId = pid != null && Number.isFinite(pid) ? pid : tasks.reduce((m, t) => Math.max(m, t.id), 0) + 1;
39533
+ tasks.push({ id: nextId, text: p.text ?? "", done: p.done === true });
39534
+ }
39535
+ }
39536
+ if (tasks.length > CHECKLIST_MAX_TASKS) {
39537
+ throw new Error(`checklist exceeds ${CHECKLIST_MAX_TASKS}-task limit (got ${tasks.length})`);
39538
+ }
39539
+ return { title: patch.title ?? cl.title, tasks };
39540
+ }
39541
+ function buildNativeChecklistPayload(p) {
39542
+ return {
39543
+ business_connection_id: p.businessConnectionId,
39544
+ chat_id: p.chatId,
39545
+ checklist: {
39546
+ title: p.title,
39547
+ tasks: p.tasks.map((t) => ({ id: t.id, text: t.text }))
39548
+ },
39549
+ ...p.replyToMessageId != null ? { reply_parameters: { message_id: p.replyToMessageId } } : {},
39550
+ ...p.protectContent === true ? { protect_content: true } : {}
39551
+ };
39552
+ }
39553
+ function buildNativeEditChecklistPayload(p) {
39554
+ return {
39555
+ business_connection_id: p.businessConnectionId,
39556
+ chat_id: p.chatId,
39557
+ message_id: p.messageId,
39558
+ checklist: {
39559
+ title: p.title,
39560
+ tasks: p.tasks.map((t) => ({ id: t.id, text: t.text }))
39561
+ }
39562
+ };
39563
+ }
39564
+ function createChecklistStore(cap = 500) {
39565
+ const map = new Map;
39566
+ return {
39567
+ get: (key) => map.get(key),
39568
+ set: (key, state) => {
39569
+ if (map.has(key))
39570
+ map.delete(key);
39571
+ map.set(key, state);
39572
+ while (map.size > cap) {
39573
+ const oldest = map.keys().next().value;
39574
+ if (oldest == null)
39575
+ break;
39576
+ map.delete(oldest);
39577
+ }
39578
+ }
39579
+ };
39580
+ }
39581
+ function checklistStoreKey(chatId, messageId) {
39582
+ return `${chatId}:${messageId}`;
39583
+ }
39584
+ async function performSendChecklist(deps, args) {
39585
+ const tasks = buildChecklistTasks(args.tasks);
39586
+ if (deps.nativeAvailable && deps.businessConnectionId) {
39587
+ try {
39588
+ const sent2 = await deps.sendNative(buildNativeChecklistPayload({
39589
+ businessConnectionId: deps.businessConnectionId,
39590
+ chatId: args.chatId,
39591
+ title: args.title,
39592
+ tasks,
39593
+ replyToMessageId: args.replyToMessageId,
39594
+ protectContent: args.protectContent
39595
+ }));
39596
+ return {
39597
+ message_id: sent2.message_id,
39598
+ mode: "native",
39599
+ state: { title: args.title, tasks, mode: "native" }
39600
+ };
39601
+ } catch (err) {
39602
+ deps.log(`native sendChecklist failed \u2014 falling back to text render: ${err}
39603
+ `);
39604
+ }
39605
+ }
39606
+ const text = renderChecklistText({ title: args.title, tasks }, { literal: deps.literalText });
39607
+ const sent = await deps.sendText(text);
39608
+ return {
39609
+ message_id: sent.message_id,
39610
+ mode: "text",
39611
+ state: { title: args.title, tasks, mode: "text" }
39612
+ };
39613
+ }
39614
+ function sendChecklistToolText(r) {
39615
+ return JSON.stringify(r.mode === "native" ? { ok: true, message_id: r.message_id, mode: "native" } : {
39616
+ ok: true,
39617
+ message_id: r.message_id,
39618
+ mode: "text",
39619
+ degraded: "text",
39620
+ note: "rendered as a formatted text message (native Telegram checklists require a Business connection) \u2014 tasks are not tappable; use update_checklist with task ids 1..N to tick them"
39621
+ });
39622
+ }
39623
+ async function performUpdateChecklist(deps, patch) {
39624
+ let base = deps.state;
39625
+ if (!base) {
39626
+ const fullReplacement = patch.title != null && (patch.tasks?.length ?? 0) > 0 && (patch.tasks ?? []).every((t) => t.text != null);
39627
+ if (!fullReplacement) {
39628
+ return {
39629
+ ok: false,
39630
+ reason: "unknown_checklist",
39631
+ hint: "no stored state for this checklist (gateway restarted?). Re-send it with send_checklist, or pass a full replacement (title + every task's text) to update_checklist."
39632
+ };
39633
+ }
39634
+ base = { title: patch.title, tasks: [], mode: "text" };
39635
+ }
39636
+ let next;
39637
+ try {
39638
+ next = applyChecklistPatch(base, patch);
39639
+ } catch (err) {
39640
+ return { ok: false, reason: "edit_failed", hint: String(err) };
39641
+ }
39642
+ const state = { title: next.title, tasks: next.tasks, mode: base.mode };
39643
+ if (base.mode === "native") {
39644
+ if (!deps.nativeAvailable || !deps.businessConnectionId) {
39645
+ return {
39646
+ ok: false,
39647
+ reason: "edit_failed",
39648
+ hint: "this checklist was sent natively but the business connection / checklist API is no longer available."
39649
+ };
39650
+ }
39651
+ try {
39652
+ await deps.editNative(buildNativeEditChecklistPayload({
39653
+ businessConnectionId: deps.businessConnectionId,
39654
+ chatId: deps.chatId,
39655
+ messageId: deps.messageId,
39656
+ title: state.title,
39657
+ tasks: state.tasks
39658
+ }));
39659
+ return { ok: true, mode: "native", state };
39660
+ } catch (err) {
39661
+ deps.log(`native editMessageChecklist failed: ${err}
39662
+ `);
39663
+ return { ok: false, reason: "edit_failed", hint: String(err) };
39664
+ }
39665
+ }
39666
+ try {
39667
+ await deps.editText(renderChecklistText(state, { literal: deps.literalText }));
39668
+ return { ok: true, mode: "text", state };
39669
+ } catch (err) {
39670
+ deps.log(`checklist text edit failed: ${err}
39671
+ `);
39672
+ return { ok: false, reason: "edit_failed", hint: String(err) };
39673
+ }
39674
+ }
39675
+ function updateChecklistToolText(r, messageId) {
39676
+ return JSON.stringify(r.ok ? { ok: true, message_id: messageId, mode: r.mode, ...r.mode === "text" ? { degraded: "text" } : {} } : { ok: false, reason: r.reason, hint: r.hint });
39677
+ }
39678
+
39507
39679
  // interrupt-marker.ts
39508
39680
  function parseInterruptMarker(text) {
39509
39681
  const trimmed = text.trimStart();
@@ -91772,6 +91944,18 @@ function computeTurnStatus(turn) {
91772
91944
  return turn.finalAnswerDelivered ? "complete" : "no_reply";
91773
91945
  }
91774
91946
  }
91947
+ function computeTurnRoute(turn) {
91948
+ switch (turn.deliveryOutcome) {
91949
+ case "failed":
91950
+ return "none";
91951
+ case "delivered":
91952
+ return "flush";
91953
+ case "suppressed":
91954
+ return "reply";
91955
+ default:
91956
+ return turn.finalAnswerDelivered ? turn.replyCalled ? "reply" : "stream" : "none";
91957
+ }
91958
+ }
91775
91959
  function buildTurnRecord(turn, endedAt) {
91776
91960
  return {
91777
91961
  ts: Math.floor(endedAt / 1000),
@@ -91780,6 +91964,11 @@ function buildTurnRecord(turn, endedAt) {
91780
91964
  tools: turn.toolCallCount ?? 0,
91781
91965
  status: computeTurnStatus(turn),
91782
91966
  turn_id: turn.turnId,
91967
+ route: computeTurnRoute({
91968
+ finalAnswerDelivered: turn.finalAnswerDelivered,
91969
+ replyCalled: turn.replyCalled ?? false,
91970
+ deliveryOutcome: turn.deliveryOutcome
91971
+ }),
91783
91972
  ...turn.landedUnconfirmed != null && turn.landedUnconfirmed > 0 ? { landed_unconfirmed: turn.landedUnconfirmed } : {}
91784
91973
  };
91785
91974
  }
@@ -99917,10 +100106,10 @@ function startOutboxSweep(deps) {
99917
100106
  }
99918
100107
 
99919
100108
  // ../src/build-info.ts
99920
- var VERSION2 = "0.19.40";
99921
- var COMMIT_SHA = "89286b22";
99922
- var COMMIT_DATE = "2026-07-31T04:28:56Z";
99923
- var LATEST_PR = 4061;
100109
+ var VERSION2 = "0.19.41";
100110
+ var COMMIT_SHA = "e77ee9c0";
100111
+ var COMMIT_DATE = "2026-07-31T09:46:10Z";
100112
+ var LATEST_PR = 4066;
99924
100113
  var COMMITS_AHEAD_OF_TAG = 0;
99925
100114
 
99926
100115
  // gateway/boot-version.ts
@@ -101975,40 +102164,21 @@ var GRAMMY_VERSION = (() => {
101975
102164
  var _rawSendChecklist;
101976
102165
  var _rawEditMessageChecklist;
101977
102166
  var CHECKLIST_API_AVAILABLE = false;
101978
- async function rawSendChecklist(args) {
101979
- if (!CHECKLIST_API_AVAILABLE) {
102167
+ async function rawSendChecklist(payload) {
102168
+ if (!CHECKLIST_API_AVAILABLE)
101980
102169
  throw new Error("sendChecklist is not available in this grammY/Telegram Bot API version");
101981
- }
101982
- const MAX_TASKS = 30;
101983
- if (args.tasks.length > MAX_TASKS) {
101984
- throw new Error(`checklist exceeds ${MAX_TASKS}-task limit (got ${args.tasks.length})`);
101985
- }
101986
- const result = await _rawSendChecklist({
101987
- chat_id: Number(args.chat_id),
101988
- title: args.title,
101989
- tasks: args.tasks.map((t) => ({ text: t.text, ...t.done != null ? { is_completed: t.done } : {} })),
101990
- ...args.message_thread_id != null ? { message_thread_id: args.message_thread_id } : {},
101991
- ...args.reply_to_message_id != null ? { reply_to_message_id: args.reply_to_message_id } : {},
101992
- ...args.protect_content === true ? { protect_content: true } : {}
101993
- });
102170
+ const result = await _rawSendChecklist(payload);
101994
102171
  return { message_id: result.message_id };
101995
102172
  }
101996
- async function rawEditMessageChecklist(args) {
101997
- if (!CHECKLIST_API_AVAILABLE) {
102173
+ async function rawEditMessageChecklist(payload) {
102174
+ if (!CHECKLIST_API_AVAILABLE)
101998
102175
  throw new Error("editMessageChecklist is not available in this grammY/Telegram Bot API version");
101999
- }
102000
- await _rawEditMessageChecklist({
102001
- chat_id: Number(args.chat_id),
102002
- message_id: Number(args.message_id),
102003
- ...args.title != null ? { title: args.title } : {},
102004
- ...args.tasks != null ? {
102005
- tasks: args.tasks.map((t) => ({
102006
- ...t.id != null ? { id: Number(t.id) } : {},
102007
- ...t.text != null ? { text: t.text } : {},
102008
- ...t.done != null ? { is_completed: t.done } : {}
102009
- }))
102010
- } : {}
102011
- });
102176
+ await _rawEditMessageChecklist(payload);
102177
+ }
102178
+ var checklistStore = createChecklistStore();
102179
+ function resolveBusinessConnectionId() {
102180
+ const v = process.env.SWITCHROOM_TELEGRAM_BUSINESS_CONNECTION_ID?.trim();
102181
+ return v ? v : undefined;
102012
102182
  }
102013
102183
  var chatLock = createChatLock();
102014
102184
  var lockedBot;
@@ -103399,6 +103569,7 @@ function emitTurnRecord(turn, endedAt) {
103399
103569
  toolCallCount: turn.toolCallCount ?? 0,
103400
103570
  turnId: turn.turnId,
103401
103571
  finalAnswerDelivered: turn.finalAnswerDelivered,
103572
+ replyCalled: turn.replyCalled,
103402
103573
  deliveryOutcome: turn.deliveryOutcome,
103403
103574
  landedUnconfirmed: turn.landedUnconfirmed
103404
103575
  }, endedAt)) + `
@@ -106723,17 +106894,24 @@ async function executeSendChecklist(args) {
106723
106894
  const protectContent = args.protect_content === true;
106724
106895
  assertAllowedChat(chat_id);
106725
106896
  const { title: redactedTitle, tasks: redactedTasks } = redactChecklistFields(title, tasks, (t) => redactOutboundText(t, "send_checklist"));
106726
- const sent = await rawSendChecklist({
106727
- chat_id,
106728
- title: redactedTitle,
106729
- tasks: redactedTasks,
106897
+ const literal = (loadAccess().parseMode ?? "html") === "text";
106898
+ const sendOpts = {
106730
106899
  ...threadId != null ? { message_thread_id: threadId } : {},
106731
- ...replyTo != null ? { reply_to_message_id: replyTo } : {},
106900
+ ...replyTo != null ? { reply_parameters: { message_id: replyTo } } : {},
106732
106901
  ...protectContent ? { protect_content: true } : {}
106733
- });
106734
- process.stderr.write(`telegram gateway: send_checklist: sent chatId=${chat_id} messageId=${sent.message_id} tasks=${tasks.length}
106902
+ };
106903
+ const result = await performSendChecklist({
106904
+ businessConnectionId: resolveBusinessConnectionId(),
106905
+ nativeAvailable: CHECKLIST_API_AVAILABLE,
106906
+ sendNative: rawSendChecklist,
106907
+ sendText: (text5) => robustApiCall(() => literal ? lockedBot.api.sendMessage(chat_id, text5, sendOpts) : lockedBot.api.sendRichMessage(chat_id, richMessage2(text5), sendOpts), { verb: "sendMessage", chat_id, threadId }),
106908
+ literalText: literal,
106909
+ log: (l) => process.stderr.write(`telegram gateway: ${l}`)
106910
+ }, { title: redactedTitle, tasks: redactedTasks, chatId: Number(chat_id), replyToMessageId: replyTo, protectContent });
106911
+ checklistStore.set(checklistStoreKey(chat_id, result.message_id), result.state);
106912
+ process.stderr.write(`telegram gateway: send_checklist: sent chatId=${chat_id} messageId=${result.message_id} tasks=${tasks.length} mode=${result.mode}
106735
106913
  `);
106736
- return { content: [{ type: "text", text: `checklist sent (id: ${sent.message_id})` }] };
106914
+ return { content: [{ type: "text", text: sendChecklistToolText(result) }] };
106737
106915
  }
106738
106916
  var linearAuthAlertLast = new Map;
106739
106917
  var LINEAR_AUTH_ALERT_COOLDOWN_MS = 21600000;
@@ -106785,10 +106963,26 @@ async function executeUpdateChecklist(args) {
106785
106963
  const tasks = args.tasks;
106786
106964
  assertAllowedChat(chat_id);
106787
106965
  const { title: redactedTitle, tasks: redactedTasks } = redactChecklistFields(title, tasks, (t) => redactOutboundText(t, "update_checklist"));
106788
- await rawEditMessageChecklist({ chat_id, message_id, title: redactedTitle, tasks: redactedTasks });
106789
- process.stderr.write(`telegram gateway: update_checklist: updated chatId=${chat_id} messageId=${message_id}
106790
- `);
106791
- return { content: [{ type: "text", text: `checklist updated (id: ${message_id})` }] };
106966
+ const literal = (loadAccess().parseMode ?? "html") === "text";
106967
+ const key = checklistStoreKey(chat_id, message_id);
106968
+ const result = await performUpdateChecklist({
106969
+ state: checklistStore.get(key),
106970
+ businessConnectionId: resolveBusinessConnectionId(),
106971
+ nativeAvailable: CHECKLIST_API_AVAILABLE,
106972
+ editNative: rawEditMessageChecklist,
106973
+ editText: async (text5) => {
106974
+ await robustApiCall(() => lockedBot.api.editMessageText(chat_id, Number(message_id), literal ? text5 : richMessage2(text5)));
106975
+ },
106976
+ literalText: literal,
106977
+ log: (l) => process.stderr.write(`telegram gateway: ${l}`),
106978
+ chatId: Number(chat_id),
106979
+ messageId: Number(message_id)
106980
+ }, { title: redactedTitle, tasks: redactedTasks });
106981
+ if (result.ok)
106982
+ checklistStore.set(key, result.state);
106983
+ process.stderr.write(`telegram gateway: update_checklist: ${result.ok ? `updated mode=${result.mode}` : `failed reason=${result.reason}`} chatId=${chat_id} messageId=${message_id}
106984
+ `);
106985
+ return { content: [{ type: "text", text: updateChecklistToolText(result, Number(message_id)) }] };
106792
106986
  }
106793
106987
  function redactOutboundText(text5, site) {
106794
106988
  const masked = redact2(text5);
@@ -25007,7 +25007,7 @@ var init_bridge = __esm(async () => {
25007
25007
  },
25008
25008
  {
25009
25009
  name: "send_checklist",
25010
- description: 'Send a native Telegram checklist (interactive task list) to a chat. Users can tick tasks directly in the Telegram app. Returns the message_id of the created checklist. The bot is notified when tasks are ticked \u2014 these arrive as channel events with kind="checklist_task_changed". Limit: 30 tasks per checklist.',
25010
+ description: 'Send a checklist (task list) to a chat. Native interactive Telegram checklists require a Business-account connection, which most deployments do not have \u2014 in that normal case the checklist is sent as a formatted TEXT message (bold title + \u2705/\u2b1c task lines) and the result carries degraded:"text": tasks are NOT tappable, and you tick them yourself via update_checklist (task ids are 1..N in send order). With a Business connection configured, the checklist is sent natively and tick events arrive as channel events with kind="checklist_task_changed". Returns JSON with message_id and mode. Limit: 30 tasks.',
25011
25011
  inputSchema: {
25012
25012
  type: "object",
25013
25013
  properties: {
@@ -25092,7 +25092,7 @@ var init_bridge = __esm(async () => {
25092
25092
  },
25093
25093
  {
25094
25094
  name: "update_checklist",
25095
- description: "Patch an existing native Telegram checklist. Supports updating the title, adding new tasks, removing tasks, or marking tasks done/undone. Tasks with an id target existing items; tasks without an id are appended. Preserves existing task ids across edits.",
25095
+ description: `Patch a checklist previously sent with send_checklist: update the title, append tasks, or mark tasks done/undone. Tasks with an id target existing items (ids are 1..N in send order for text-mode checklists); tasks without an id are appended. Removal is not supported. The edit re-renders in the mode the checklist was sent in (text for most deployments). Returns JSON { ok, ... }; after a gateway restart the stored task state may be gone \u2014 then it returns ok:false with reason "unknown_checklist" unless you pass a full replacement (title + every task's text).`,
25096
25096
  inputSchema: {
25097
25097
  type: "object",
25098
25098
  properties: {