switchroom 0.14.53 → 0.14.55

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.
@@ -11049,6 +11049,7 @@ var TelegramChannelSchema = exports_external.object({
11049
11049
  rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
11050
11050
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
11051
11051
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
11052
+ clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
11052
11053
  hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md, HEARTBEAT.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
11053
11054
  orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
11054
11055
  cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
@@ -11049,6 +11049,7 @@ var TelegramChannelSchema = exports_external.object({
11049
11049
  rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
11050
11050
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
11051
11051
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
11052
+ clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
11052
11053
  hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md, HEARTBEAT.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
11053
11054
  orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
11054
11055
  cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
@@ -11797,6 +11797,7 @@ var TelegramChannelSchema = exports_external.object({
11797
11797
  rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
11798
11798
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI \u2014 " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events \u2014 stable " + "order, per-tool status emojis, fires only on semantic transitions."),
11799
11799
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
11800
+ clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message \u2014 Reading X, Searching the web for Y, \u2026) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) \u2014 no post-then-delete. Per-agent " + "override; cascades defaults \u2192 profile \u2192 agent (per-key)."),
11800
11801
  hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md, HEARTBEAT.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
11801
11802
  orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
11802
11803
  cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
@@ -13613,6 +13613,7 @@ var init_schema = __esm(() => {
13613
13613
  rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
13614
13614
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI \u2014 " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events \u2014 stable " + "order, per-tool status emojis, fires only on semantic transitions."),
13615
13615
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
13616
+ clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message \u2014 Reading X, Searching the web for Y, \u2026) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) \u2014 no post-then-delete. Per-agent " + "override; cascades defaults \u2192 profile \u2192 agent (per-key)."),
13616
13617
  hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md, HEARTBEAT.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
13617
13618
  orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
13618
13619
  cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
@@ -49462,8 +49463,8 @@ var {
49462
49463
  } = import__.default;
49463
49464
 
49464
49465
  // src/build-info.ts
49465
- var VERSION = "0.14.53";
49466
- var COMMIT_SHA = "40ba8e59";
49466
+ var VERSION = "0.14.55";
49467
+ var COMMIT_SHA = "dc589fea";
49467
49468
 
49468
49469
  // src/cli/agent.ts
49469
49470
  init_source();
@@ -50868,6 +50869,9 @@ function channelsToEnv(agent) {
50868
50869
  if (tg.edit_budget_threshold !== undefined) {
50869
50870
  out.SWITCHROOM_TG_EDIT_BUDGET_THRESHOLD = String(tg.edit_budget_threshold);
50870
50871
  }
50872
+ if (tg.clear_status_on_completion !== undefined) {
50873
+ out.SWITCHROOM_TG_CLEAR_STATUS_ON_COMPLETION = tg.clear_status_on_completion ? "1" : "0";
50874
+ }
50871
50875
  return out;
50872
50876
  }
50873
50877
  function buildRepoEnvVars(_agentName, agentDir, agent) {
@@ -13784,6 +13784,7 @@ var TelegramChannelSchema = exports_external.object({
13784
13784
  rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
13785
13785
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
13786
13786
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
13787
+ clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
13787
13788
  hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md, HEARTBEAT.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
13788
13789
  orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
13789
13790
  cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
@@ -11370,6 +11370,7 @@ var init_schema = __esm(() => {
11370
11370
  rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
11371
11371
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
11372
11372
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
11373
+ clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
11373
11374
  hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md, HEARTBEAT.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
11374
11375
  orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
11375
11376
  cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
@@ -11370,6 +11370,7 @@ var init_schema = __esm(() => {
11370
11370
  rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
11371
11371
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI — " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events — stable " + "order, per-tool status emojis, fires only on semantic transitions."),
11372
11372
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
11373
+ clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message — Reading X, Searching the web for Y, …) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) — no post-then-delete. Per-agent " + "override; cascades defaults → profile → agent (per-key)."),
11373
11374
  hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md, HEARTBEAT.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
11374
11375
  orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
11375
11376
  cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.53",
3
+ "version": "0.14.55",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,24 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
33
33
  # same path the rest of start.sh + the MCP sidecar expects.
34
34
  export TELEGRAM_STATE_DIR="{{agentDir}}/telegram"
35
35
 
36
+ # Gateway-consumed env MUST be exported HERE, before the gateway fork
37
+ # below. The gateway daemon reads channels.telegram.* knobs (and any
38
+ # agent env) from process.env at startup — e.g. SWITCHROOM_TG_STREAM_
39
+ # THROTTLE_MS, SWITCHROOM_TG_EDIT_BUDGET_THRESHOLD,
40
+ # SWITCHROOM_TG_CLEAR_STATUS_ON_COMPLETION. The inner tmux pass
41
+ # re-exports the SAME user-env block below for claude, because tmux
42
+ # does not reliably propagate arbitrary env across the
43
+ # re-exec. Without this hoist these vars only ever reach the inner
44
+ # (claude) pass and the already-forked gateway never sees them — every
45
+ # channels.telegram env knob silently defaults on docker agents (the
46
+ # start.sh env-fork landmine, same class as #1997 /
47
+ # SWITCHROOM_HANDOFF_SHOW_LINE). Pinned by the scaffold-order test
48
+ # asserting these exports precede the gateway-fork line.
49
+ {{#if userEnvQuoted}}
50
+ {{#each userEnvQuoted}} export {{@key}}={{{this}}}
51
+ {{/each}}
52
+ {{/if}}
53
+
36
54
  # Tiny in-process supervisor: runs cmd in a respawn loop with
37
55
  # exponential backoff (1→2→4…→60s cap) and NEVER permanently gives
38
56
  # up. Rationale (RFC J / install-validation 2026-05-17): the
@@ -23779,6 +23779,7 @@ var init_schema = __esm(() => {
23779
23779
  rate_limit_ms: exports_external.number().optional().describe("Minimum delay between outgoing messages in ms"),
23780
23780
  stream_mode: exports_external.enum(["pty", "checklist"]).optional().describe("How live progress is streamed to Telegram during a turn. " + "'pty' (default) surfaces text snapshots of Claude Code's TUI \u2014 " + "compatible but can flicker as Ink re-renders. 'checklist' drives " + "a structured progress card from session-tail events \u2014 stable " + "order, per-tool status emojis, fires only on semantic transitions."),
23781
23781
  stream_throttle_ms: exports_external.number().int().nonnegative().optional().describe("Throttle window in ms between successive stream edits (or " + "sendMessageDraft tics) during a turn. Lower = more responsive " + "stream, higher = fewer API calls. Floored at 250 by draft-stream " + "itself. Default 300 for draft transport (DMs) and 1000 for " + "message transport (groups/forums). Override per-agent if a " + "particular agent needs snappier or quieter streaming."),
23782
+ clear_status_on_completion: exports_external.boolean().optional().describe("When true, the live activity/status feed (the in-place 'what it's " + "doing' message \u2014 Reading X, Searching the web for Y, \u2026) is DELETED " + "when the turn's final answer lands, so only the reply remains. " + "Default false: the status message is left in the chat as a record " + "(its last step marked done) \u2014 no post-then-delete. Per-agent " + "override; cascades defaults \u2192 profile \u2192 agent (per-key)."),
23782
23783
  hotReloadStable: exports_external.boolean().optional().describe("If true, the stable workspace prefix (AGENTS.md, SOUL.md, USER.md, " + "IDENTITY.md, TOOLS.md, HEARTBEAT.md) is re-injected on every turn via " + "the UserPromptSubmit hook instead of baked into --append-system-prompt " + "at session start. Lets workspace edits propagate without a restart. " + "Costs ~5-10% per-turn latency/spend since the stable prefix is no " + "longer prompt-cached."),
23783
23784
  orphan_promotion_ms: exports_external.number().int().nonnegative().optional().describe("How long (ms) a parent turn waits for a sub-agent JSONL watcher " + "to deliver sub_agent_started before the heartbeat promotes the spawn " + "to a synthesised 'running' row. Default 5000. Set to 0 to disable " + "orphan promotion entirely."),
23784
23785
  cold_sub_agent_threshold_ms: exports_external.number().int().nonnegative().optional().describe("JSONL-cold threshold (ms). When a running sub-agent emits no events " + "for this long, the heartbeat synthesises a turn_end for it so the " + "deferred-completion path can proceed. Default 30000. Set to 0 to " + "disable the synthetic close."),
@@ -32728,7 +32729,7 @@ var MIRROR_MAX_LINES = 6;
32728
32729
  function escapeFeedHtml(s) {
32729
32730
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
32730
32731
  }
32731
- function renderActivityFeed(lines) {
32732
+ function renderActivityFeed(lines, final = false) {
32732
32733
  if (lines.length === 0)
32733
32734
  return null;
32734
32735
  const shown = lines.slice(-MIRROR_MAX_LINES);
@@ -32739,7 +32740,7 @@ function renderActivityFeed(lines) {
32739
32740
  const lastIdx = shown.length - 1;
32740
32741
  shown.forEach((l, i) => {
32741
32742
  const esc = escapeFeedHtml(l);
32742
- out.push(i === lastIdx ? `<b>\u2192 ${esc}</b>` : `<i>\u2713 ${esc}</i>`);
32743
+ out.push(i === lastIdx && !final ? `<b>\u2192 ${esc}</b>` : `<i>\u2713 ${esc}</i>`);
32743
32744
  });
32744
32745
  return out.join(`
32745
32746
  `);
@@ -32747,10 +32748,10 @@ function renderActivityFeed(lines) {
32747
32748
  var NESTED_MAX_LINES = 4;
32748
32749
  var NESTED_LINE_MAX = 90;
32749
32750
  var NESTED_PREFIX = " \u21b3 ";
32750
- function renderActivityFeedWithNested(lines, childLines) {
32751
+ function renderActivityFeedWithNested(lines, childLines, final = false) {
32751
32752
  const children = childLines.map((s) => s.trim()).filter((s) => s.length > 0);
32752
32753
  if (children.length === 0)
32753
- return renderActivityFeed(lines);
32754
+ return renderActivityFeed(lines, final);
32754
32755
  const out = [];
32755
32756
  const shownParent = lines.slice(-MIRROR_MAX_LINES);
32756
32757
  const hiddenParent = lines.length - shownParent.length;
@@ -32766,7 +32767,7 @@ function renderActivityFeedWithNested(lines, childLines) {
32766
32767
  shownChild.forEach((l, i) => {
32767
32768
  const t = l.length > NESTED_LINE_MAX ? l.slice(0, NESTED_LINE_MAX - 1) + "\u2026" : l;
32768
32769
  const esc = escapeFeedHtml(t);
32769
- out.push(i === lastChildIdx ? `${NESTED_PREFIX}<b>\u2192 ${esc}</b>` : `${NESTED_PREFIX}<i>${esc}</i>`);
32770
+ out.push(i === lastChildIdx && !final ? `${NESTED_PREFIX}<b>\u2192 ${esc}</b>` : `${NESTED_PREFIX}<i>${esc}</i>`);
32770
32771
  });
32771
32772
  return out.length > 0 ? out.join(`
32772
32773
  `) : null;
@@ -52166,10 +52167,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52166
52167
  }
52167
52168
 
52168
52169
  // ../src/build-info.ts
52169
- var VERSION = "0.14.53";
52170
- var COMMIT_SHA = "40ba8e59";
52171
- var COMMIT_DATE = "2026-06-03T13:56:26Z";
52172
- var LATEST_PR = 2131;
52170
+ var VERSION = "0.14.55";
52171
+ var COMMIT_SHA = "dc589fea";
52172
+ var COMMIT_DATE = "2026-06-03T21:05:15Z";
52173
+ var LATEST_PR = 2136;
52173
52174
  var COMMITS_AHEAD_OF_TAG = 0;
52174
52175
 
52175
52176
  // gateway/boot-version.ts
@@ -54317,6 +54318,13 @@ var STREAM_THROTTLE_MS_OVERRIDE = (() => {
54317
54318
  })();
54318
54319
  var TURN_FLUSH_SAFETY_ENABLED = isTurnFlushSafetyEnabled();
54319
54320
  var ANSWER_STREAM_VISIBLE_ENABLED = parseVisibleAnswerStreamEnabled(process.env.SWITCHROOM_VISIBLE_ANSWER_STREAM);
54321
+ var CLEAR_STATUS_ON_COMPLETION = (() => {
54322
+ const raw = process.env.SWITCHROOM_TG_CLEAR_STATUS_ON_COMPLETION;
54323
+ if (raw == null)
54324
+ return false;
54325
+ const v = raw.trim().toLowerCase();
54326
+ return v === "1" || v === "true" || v === "on" || v === "yes";
54327
+ })();
54320
54328
  var progressDriver = null;
54321
54329
  var unpinProgressCardForChat = null;
54322
54330
  var getPinnedProgressCardMessageId = null;
@@ -56874,12 +56882,12 @@ function closeProgressLane(chatId, threadId) {
56874
56882
  }
56875
56883
  }
56876
56884
  var FOREGROUND_SUBAGENT_ACCUM_MAX = 12;
56877
- function composeTurnActivity(turn) {
56885
+ function composeTurnActivity(turn, final = false) {
56878
56886
  const childLines = [];
56879
56887
  for (const narrative of turn.foregroundSubAgents.values()) {
56880
56888
  childLines.push(...narrative);
56881
56889
  }
56882
- return renderActivityFeedWithNested(turn.mirrorLines, childLines);
56890
+ return renderActivityFeedWithNested(turn.mirrorLines, childLines, final);
56883
56891
  }
56884
56892
  async function drainActivitySummary(turn) {
56885
56893
  try {
@@ -56923,13 +56931,28 @@ function clearActivitySummary(turn) {
56923
56931
  const thread = turn.sessionThreadId;
56924
56932
  const inFlight = turn.activityInFlight ?? Promise.resolve();
56925
56933
  inFlight.then(async () => {
56926
- if (turn.activityMessageId != null) {
56927
- const id = turn.activityMessageId;
56928
- turn.activityMessageId = null;
56934
+ if (turn.activityMessageId == null)
56935
+ return;
56936
+ const id = turn.activityMessageId;
56937
+ turn.activityMessageId = null;
56938
+ if (CLEAR_STATUS_ON_COMPLETION) {
56929
56939
  try {
56930
56940
  await robustApiCall(() => bot.api.deleteMessage(chat, id), { chat_id: chat, ...thread != null ? { threadId: thread } : {}, verb: "activity-summary.delete" });
56931
56941
  } catch (err) {
56932
56942
  process.stderr.write(`telegram gateway: activity-summary delete failed: ${err}
56943
+ `);
56944
+ }
56945
+ return;
56946
+ }
56947
+ const finalHtml = composeTurnActivity(turn, true);
56948
+ if (finalHtml == null)
56949
+ return;
56950
+ try {
56951
+ await robustApiCall(() => bot.api.editMessageText(chat, id, finalHtml, { parse_mode: "HTML" }), { chat_id: chat, ...thread != null ? { threadId: thread } : {}, verb: "activity-summary.finalize" });
56952
+ } catch (err) {
56953
+ const msg = err instanceof Error ? err.message : String(err);
56954
+ if (!msg.includes("message is not modified")) {
56955
+ process.stderr.write(`telegram gateway: activity-summary finalize failed: ${msg}
56933
56956
  `);
56934
56957
  }
56935
56958
  }
@@ -3767,6 +3767,19 @@ const ANSWER_STREAM_VISIBLE_ENABLED = parseVisibleAnswerStreamEnabled(
3767
3767
  process.env.SWITCHROOM_VISIBLE_ANSWER_STREAM,
3768
3768
  )
3769
3769
 
3770
+ // Whether to DELETE the activity/status feed when the final answer lands.
3771
+ // Default OFF (2026-06-04, operator request): the status message stays in the
3772
+ // chat as a record (finalized to an all-done render) instead of being deleted
3773
+ // — no post-then-delete flicker. Opt IN per agent via
3774
+ // channels.telegram.clear_status_on_completion: true (→ this env). See the
3775
+ // clearActivitySummary doc-comment.
3776
+ const CLEAR_STATUS_ON_COMPLETION = (() => {
3777
+ const raw = process.env.SWITCHROOM_TG_CLEAR_STATUS_ON_COMPLETION
3778
+ if (raw == null) return false
3779
+ const v = raw.trim().toLowerCase()
3780
+ return v === '1' || v === 'true' || v === 'on' || v === 'yes'
3781
+ })()
3782
+
3770
3783
  // Activity feed. The gateway streams a live "what it's doing" tool-activity
3771
3784
  // feed for every turn. The PreToolUse sidecar emits a `tool_label` per tool
3772
3785
  // call (flush-independent, so it stays real-time on fast/clustered-tool
@@ -8015,12 +8028,12 @@ const FOREGROUND_SUBAGENT_ACCUM_MAX = 12
8015
8028
  * order; the single-sub-agent common case nests precisely under its
8016
8029
  * Delegating line.
8017
8030
  */
8018
- function composeTurnActivity(turn: CurrentTurn): string | null {
8031
+ function composeTurnActivity(turn: CurrentTurn, final = false): string | null {
8019
8032
  const childLines: string[] = []
8020
8033
  for (const narrative of turn.foregroundSubAgents.values()) {
8021
8034
  childLines.push(...narrative)
8022
8035
  }
8023
- return renderActivityFeedWithNested(turn.mirrorLines, childLines)
8036
+ return renderActivityFeedWithNested(turn.mirrorLines, childLines, final)
8024
8037
  }
8025
8038
 
8026
8039
  /**
@@ -8094,24 +8107,30 @@ async function drainActivitySummary(turn: CurrentTurn): Promise<void> {
8094
8107
  }
8095
8108
 
8096
8109
  /**
8097
- * Clear the activity summary when the model's reply tool takes over
8098
- * as the authoritative surface. Awaits any in-flight render so we
8099
- * don't race a stale write against the clear, then deletes the activity
8100
- * message. Idempotent + best-effort failure stderr-logs but does not
8101
- * block.
8110
+ * Reconcile the activity summary when the model's reply tool takes over as the
8111
+ * authoritative surface. Awaits any in-flight render so we don't race a stale
8112
+ * write, then EITHER:
8113
+ * - FINALIZE (default, CLEAR_STATUS_ON_COMPLETION=false): edit the message to
8114
+ * a terminal all-done render (no "→ in-progress" line) and stop tracking it
8115
+ * — the status stays in the chat as a record beside the reply. No delete.
8116
+ * - DELETE (CLEAR_STATUS_ON_COMPLETION=true, opt-in via
8117
+ * channels.telegram.clear_status_on_completion): remove the message so only
8118
+ * the reply remains (the pre-2026-06 behaviour).
8119
+ * Idempotent + best-effort — failures stderr-log but don't block.
8102
8120
  *
8103
- * Called on the first reply (hand-off to the answer) and again at
8104
- * turn_end (the no-reply safety net), so the user sees the real reply
8105
- * land in the same beat the summary disappears.
8121
+ * Called on the first reply (hand-off) and again at turn_end (no-reply safety
8122
+ * net); finalize edits are idempotent (a 'message is not modified' on the
8123
+ * second call is swallowed).
8106
8124
  */
8107
8125
  function clearActivitySummary(turn: CurrentTurn): void {
8108
8126
  const chat = turn.sessionChatId
8109
8127
  const thread = turn.sessionThreadId
8110
8128
  const inFlight = turn.activityInFlight ?? Promise.resolve()
8111
8129
  void inFlight.then(async () => {
8112
- if (turn.activityMessageId != null) {
8113
- const id = turn.activityMessageId
8114
- turn.activityMessageId = null
8130
+ if (turn.activityMessageId == null) return
8131
+ const id = turn.activityMessageId
8132
+ turn.activityMessageId = null
8133
+ if (CLEAR_STATUS_ON_COMPLETION) {
8115
8134
  try {
8116
8135
  await robustApiCall(
8117
8136
  () => bot.api.deleteMessage(chat, id),
@@ -8120,6 +8139,22 @@ function clearActivitySummary(turn: CurrentTurn): void {
8120
8139
  } catch (err) {
8121
8140
  process.stderr.write(`telegram gateway: activity-summary delete failed: ${err}\n`)
8122
8141
  }
8142
+ return
8143
+ }
8144
+ // Default: leave the status message as a record, edited to a terminal
8145
+ // all-done state so it doesn't freeze on a misleading "→ in-progress" line.
8146
+ const finalHtml = composeTurnActivity(turn, true)
8147
+ if (finalHtml == null) return
8148
+ try {
8149
+ await robustApiCall(
8150
+ () => bot.api.editMessageText(chat, id, finalHtml, { parse_mode: 'HTML' }),
8151
+ { chat_id: chat, ...(thread != null ? { threadId: thread } : {}), verb: 'activity-summary.finalize' },
8152
+ )
8153
+ } catch (err) {
8154
+ const msg = err instanceof Error ? err.message : String(err)
8155
+ if (!msg.includes('message is not modified')) {
8156
+ process.stderr.write(`telegram gateway: activity-summary finalize failed: ${msg}\n`)
8157
+ }
8123
8158
  }
8124
8159
  })
8125
8160
  }
@@ -126,6 +126,26 @@ describe("appendActivityLine + renderActivityFeed — accumulating activity feed
126
126
  it("renderActivityFeed returns null on empty", () => {
127
127
  expect(renderActivityFeed([])).toBeNull();
128
128
  });
129
+
130
+ // final=true: the persisted "status stays" terminal render — the feed is
131
+ // left in the chat when clear_status_on_completion=false, so the newest line
132
+ // must read done (✓), not a frozen "→ in-progress".
133
+ it("final=true renders the newest line as done (✓), not in-progress (→)", () => {
134
+ const lines = ["Reading a.ts", "Searching memory", "Running a command"];
135
+ const out = renderActivityFeed(lines, true)!;
136
+ expect(out).toBe(
137
+ "<i>✓ Reading a.ts</i>\n<i>✓ Searching memory</i>\n<i>✓ Running a command</i>",
138
+ );
139
+ expect(out).not.toContain("→"); // no in-progress arrow anywhere
140
+ });
141
+
142
+ it("final=true on a single line is also done (✓)", () => {
143
+ expect(renderActivityFeed(["Reading a.ts"], true)).toBe("<i>✓ Reading a.ts</i>");
144
+ });
145
+
146
+ it("final defaults false (live render keeps the → in-progress newest line)", () => {
147
+ expect(renderActivityFeed(["Reading a.ts"])).toBe("<b>→ Reading a.ts</b>");
148
+ });
129
149
  });
130
150
 
131
151
  describe("appendActivityLabel — precomputed label feed (tool_label path)", () => {
@@ -186,4 +206,20 @@ describe("renderActivityFeedWithNested — foreground sub-agent nesting (Model A
186
206
  const out = renderActivityFeedWithNested(["Delegating: x"], ["touch <a> & <b>"])!;
187
207
  expect(out).toContain(" ↳ <b>→ touch &lt;a&gt; &amp; &lt;b&gt;</b>");
188
208
  });
209
+
210
+ it("final=true: the nested newest step renders done (✓), not in-progress (→)", () => {
211
+ const out = renderActivityFeedWithNested(
212
+ ["Delegating: x"],
213
+ ["Reading schema.ts", "Looking for foreign keys"],
214
+ true,
215
+ )!;
216
+ expect(out).toContain(" ↳ <i>Looking for foreign keys</i>"); // newest now italic done
217
+ expect(out).not.toContain("→"); // no in-progress arrow in the finalized feed
218
+ });
219
+
220
+ it("final=true with no children delegates to the finalized flat render", () => {
221
+ expect(renderActivityFeedWithNested(["Reading a.ts"], [], true)).toBe(
222
+ "<i>✓ Reading a.ts</i>",
223
+ );
224
+ });
189
225
  });
@@ -200,7 +200,7 @@ function escapeFeedHtml(s: string): string {
200
200
  * `✓ +N earlier…` header when the turn ran longer. Returns null when empty.
201
201
  * Callers send the result verbatim — do NOT re-escape or re-wrap it.
202
202
  */
203
- export function renderActivityFeed(lines: string[]): string | null {
203
+ export function renderActivityFeed(lines: string[], final = false): string | null {
204
204
  if (lines.length === 0) return null;
205
205
  const shown = lines.slice(-MIRROR_MAX_LINES);
206
206
  const hidden = lines.length - shown.length;
@@ -208,10 +208,12 @@ export function renderActivityFeed(lines: string[]): string | null {
208
208
  if (hidden > 0) out.push(`<i>✓ +${hidden} earlier…</i>`);
209
209
  const lastIdx = shown.length - 1;
210
210
  // Newest line = in-progress step (bold, →); earlier = done (italic, ✓).
211
+ // `final` (turn complete, feed left as a record): ALL lines render done (✓)
212
+ // so the persisted message doesn't freeze on a misleading "→ in-progress".
211
213
  // Returns ready Telegram HTML — callers must NOT re-escape or re-wrap it.
212
214
  shown.forEach((l, i) => {
213
215
  const esc = escapeFeedHtml(l);
214
- out.push(i === lastIdx ? `<b>→ ${esc}</b>` : `<i>✓ ${esc}</i>`);
216
+ out.push(i === lastIdx && !final ? `<b>→ ${esc}</b>` : `<i>✓ ${esc}</i>`);
215
217
  });
216
218
  return out.join("\n");
217
219
  }
@@ -245,9 +247,10 @@ const NESTED_PREFIX = " ↳ ";
245
247
  export function renderActivityFeedWithNested(
246
248
  lines: string[],
247
249
  childLines: string[],
250
+ final = false,
248
251
  ): string | null {
249
252
  const children = childLines.map((s) => s.trim()).filter((s) => s.length > 0);
250
- if (children.length === 0) return renderActivityFeed(lines);
253
+ if (children.length === 0) return renderActivityFeed(lines, final);
251
254
 
252
255
  const out: string[] = [];
253
256
  const shownParent = lines.slice(-MIRROR_MAX_LINES);
@@ -259,11 +262,13 @@ export function renderActivityFeedWithNested(
259
262
  const hiddenChild = children.length - shownChild.length;
260
263
  if (hiddenChild > 0) out.push(`${NESTED_PREFIX}<i>+${hiddenChild} earlier…</i>`);
261
264
  const lastChildIdx = shownChild.length - 1;
265
+ // `final`: the nested newest step also renders done (✓) so the left-behind
266
+ // feed reads as completed, not stuck on a "→ in-progress" child step.
262
267
  shownChild.forEach((l, i) => {
263
268
  const t = l.length > NESTED_LINE_MAX ? l.slice(0, NESTED_LINE_MAX - 1) + "…" : l;
264
269
  const esc = escapeFeedHtml(t);
265
270
  out.push(
266
- i === lastChildIdx
271
+ i === lastChildIdx && !final
267
272
  ? `${NESTED_PREFIX}<b>→ ${esc}</b>`
268
273
  : `${NESTED_PREFIX}<i>${esc}</i>`,
269
274
  );