switchroom 0.14.90 → 0.14.91

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.
@@ -49815,8 +49815,8 @@ var {
49815
49815
  } = import__.default;
49816
49816
 
49817
49817
  // src/build-info.ts
49818
- var VERSION = "0.14.90";
49819
- var COMMIT_SHA = "6386ff19";
49818
+ var VERSION = "0.14.91";
49819
+ var COMMIT_SHA = "e938daab";
49820
49820
 
49821
49821
  // src/cli/agent.ts
49822
49822
  init_source();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.90",
3
+ "version": "0.14.91",
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": {
@@ -52900,10 +52900,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52900
52900
  }
52901
52901
 
52902
52902
  // ../src/build-info.ts
52903
- var VERSION = "0.14.90";
52904
- var COMMIT_SHA = "6386ff19";
52905
- var COMMIT_DATE = "2026-06-08T00:05:33Z";
52906
- var LATEST_PR = 2235;
52903
+ var VERSION = "0.14.91";
52904
+ var COMMIT_SHA = "e938daab";
52905
+ var COMMIT_DATE = "2026-06-09T03:14:21Z";
52906
+ var LATEST_PR = 2241;
52907
52907
  var COMMITS_AHEAD_OF_TAG = 0;
52908
52908
 
52909
52909
  // gateway/boot-version.ts
@@ -53545,6 +53545,21 @@ function resolveWorkerFeedDispatch(sub, watcherDescription) {
53545
53545
  };
53546
53546
  }
53547
53547
 
53548
+ // gateway/subagent-status-surface.ts
53549
+ function resolveSubagentStatusSurface(input) {
53550
+ if (!input.isBackground) {
53551
+ if (input.liveTurnPresent)
53552
+ return "nest";
53553
+ if (!input.orphanStatusEnabled)
53554
+ return "skip";
53555
+ return input.workerFeedEnabled ? "worker-feed" : "skip";
53556
+ }
53557
+ return input.workerFeedEnabled ? "worker-feed" : "legacy-relay";
53558
+ }
53559
+ function isOrphanSubagentStatusEnabled(envVal) {
53560
+ return envVal !== "0";
53561
+ }
53562
+
53548
53563
  // gateway/resolve-calling-subagent.ts
53549
53564
  function resolveCallingSubagent(opts) {
53550
53565
  if (opts.db == null)
@@ -64569,6 +64584,7 @@ var didOneTimeSetup = false;
64569
64584
  if (watcherAgentDir != null) {
64570
64585
  const workerFeedEnabled = isWorkerActivityFeedEnabled(process.env.SWITCHROOM_WORKER_ACTIVITY_FEED);
64571
64586
  const foregroundNestingEnabled = process.env.SWITCHROOM_FOREGROUND_SUBAGENT_NESTING !== "0";
64587
+ const orphanStatusEnabled = isOrphanSubagentStatusEnabled(process.env.SWITCHROOM_ORPHAN_SUBAGENT_STATUS);
64572
64588
  const workerActivityFeed = createWorkerActivityFeed({
64573
64589
  bot: {
64574
64590
  sendMessage: async (cid, text, sendOpts) => {
@@ -64650,6 +64666,22 @@ var didOneTimeSetup = false;
64650
64666
  }
64651
64667
  }
64652
64668
  }
64669
+ return;
64670
+ }
64671
+ if (resolveSubagentStatusSurface({
64672
+ isBackground: false,
64673
+ liveTurnPresent: false,
64674
+ workerFeedEnabled,
64675
+ orphanStatusEnabled
64676
+ }) === "worker-feed") {
64677
+ workerActivityFeed.finish(agentId, {
64678
+ description: dispatch.feedDescription,
64679
+ lastTool: null,
64680
+ toolCount,
64681
+ latestSummary: resultText,
64682
+ elapsedMs: durationMs,
64683
+ state: outcome === "failed" ? "failed" : "done"
64684
+ });
64653
64685
  }
64654
64686
  return;
64655
64687
  }
@@ -64716,6 +64748,26 @@ var didOneTimeSetup = false;
64716
64748
  }
64717
64749
  const isBackground = dispatch.isBackground;
64718
64750
  if (!isBackground) {
64751
+ const surface = resolveSubagentStatusSurface({
64752
+ isBackground: false,
64753
+ liveTurnPresent: currentTurn != null,
64754
+ workerFeedEnabled,
64755
+ orphanStatusEnabled
64756
+ });
64757
+ if (surface === "worker-feed") {
64758
+ const origin = resolveSubagentOriginChat(agentId);
64759
+ workerActivityFeed.update(agentId, origin?.chatId || fleetChatId || (loadAccess().allowFrom[0] ?? ""), {
64760
+ description: dispatch.feedDescription,
64761
+ lastTool,
64762
+ toolCount,
64763
+ latestSummary,
64764
+ elapsedMs,
64765
+ state: "running"
64766
+ }, origin?.threadId);
64767
+ return;
64768
+ }
64769
+ if (surface !== "nest")
64770
+ return;
64719
64771
  const turn = currentTurn;
64720
64772
  if (turn == null)
64721
64773
  return;
@@ -471,6 +471,10 @@ import {
471
471
  } from './resume-inbound-builder.js'
472
472
  import { applySubagentsSchema, getSubagentByJsonlId } from '../registry/subagents-schema.js'
473
473
  import { resolveWorkerFeedDispatch, type WorkerFeedDispatch } from './worker-feed-dispatch.js'
474
+ import {
475
+ resolveSubagentStatusSurface,
476
+ isOrphanSubagentStatusEnabled,
477
+ } from './subagent-status-surface.js'
474
478
  import { formatIdleFooter } from '../idle-footer.js'
475
479
  import { resolveCallingSubagent } from './resolve-calling-subagent.js'
476
480
 
@@ -20410,6 +20414,11 @@ void (async () => {
20410
20414
  // compose draft, so no answer-stream contention). The kill-switch
20411
20415
  // disables only the nesting; the parent's own feed is unaffected.
20412
20416
  const foregroundNestingEnabled = process.env.SWITCHROOM_FOREGROUND_SUBAGENT_NESTING !== '0'
20417
+ // Orphaned-foreground status (2026-06-09): a FOREGROUND sub-agent
20418
+ // with no live parent turn to nest into (dispatched outside a turn,
20419
+ // or the turn ended while it kept running — extended autonomous
20420
+ // work) is surfaced via the worker feed instead of vanishing.
20421
+ const orphanStatusEnabled = isOrphanSubagentStatusEnabled(process.env.SWITCHROOM_ORPHAN_SUBAGENT_STATUS)
20413
20422
  const workerActivityFeed = createWorkerActivityFeed({
20414
20423
  bot: {
20415
20424
  sendMessage: async (cid, text, sendOpts) => {
@@ -20609,6 +20618,29 @@ void (async () => {
20609
20618
  }
20610
20619
  }
20611
20620
  }
20621
+ return
20622
+ }
20623
+ // Not nested → an orphaned foreground sub-agent that was
20624
+ // surfaced via the worker feed (no live turn to nest into):
20625
+ // finalize its message (no-op if none was posted). A
20626
+ // foreground result returns inline as the Task tool result, so
20627
+ // there is no handback to deliver — return after.
20628
+ if (
20629
+ resolveSubagentStatusSurface({
20630
+ isBackground: false,
20631
+ liveTurnPresent: false,
20632
+ workerFeedEnabled,
20633
+ orphanStatusEnabled,
20634
+ }) === 'worker-feed'
20635
+ ) {
20636
+ void workerActivityFeed.finish(agentId, {
20637
+ description: dispatch.feedDescription,
20638
+ lastTool: null,
20639
+ toolCount,
20640
+ latestSummary: resultText,
20641
+ elapsedMs: durationMs,
20642
+ state: outcome === 'failed' ? 'failed' : 'done',
20643
+ })
20612
20644
  }
20613
20645
  return
20614
20646
  }
@@ -20738,8 +20770,39 @@ void (async () => {
20738
20770
  // activity draft rather than a separate worker message. Pure
20739
20771
  // jsonl-tail → render (no model call), inside the
20740
20772
  // subscription-honest boundary.
20773
+ //
20774
+ // But a foreground sub-agent with NO live turn to nest into
20775
+ // (dispatched outside a turn, or the turn ended while it kept
20776
+ // running — extended autonomous work) has nowhere to nest, and
20777
+ // pre-fix it silently returned here → invisible. Route through
20778
+ // the proven decision: an orphaned foreground sub-agent goes to
20779
+ // the worker feed (owner-DM fallback), not into the void.
20780
+ const surface = resolveSubagentStatusSurface({
20781
+ isBackground: false,
20782
+ liveTurnPresent: currentTurn != null,
20783
+ workerFeedEnabled,
20784
+ orphanStatusEnabled,
20785
+ })
20786
+ if (surface === 'worker-feed') {
20787
+ const origin = resolveSubagentOriginChat(agentId)
20788
+ void workerActivityFeed.update(
20789
+ agentId,
20790
+ origin?.chatId || fleetChatId || (loadAccess().allowFrom[0] ?? ''),
20791
+ {
20792
+ description: dispatch.feedDescription,
20793
+ lastTool,
20794
+ toolCount,
20795
+ latestSummary,
20796
+ elapsedMs,
20797
+ state: 'running',
20798
+ },
20799
+ origin?.threadId,
20800
+ )
20801
+ return
20802
+ }
20803
+ if (surface !== 'nest') return // 'skip' — orphan-status off
20741
20804
  const turn = currentTurn
20742
- if (turn == null) return
20805
+ if (turn == null) return // defensive: 'nest' implies a live turn
20743
20806
  // Render regardless of `replyCalled` — a foreground Task
20744
20807
  // blocks the parent, so any reply seen while it runs is an
20745
20808
  // interim ack, never the final answer. Gating on replyCalled
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import {
3
+ resolveSubagentStatusSurface,
4
+ isOrphanSubagentStatusEnabled,
5
+ type SubagentStatusSurface,
6
+ type SubagentStatusSurfaceInput,
7
+ } from './subagent-status-surface.js'
8
+
9
+ // ── Human-readable map ──────────────────────────────────────────────────────
10
+ describe('resolveSubagentStatusSurface', () => {
11
+ const base: SubagentStatusSurfaceInput = {
12
+ isBackground: false,
13
+ liveTurnPresent: true,
14
+ workerFeedEnabled: true,
15
+ orphanStatusEnabled: true,
16
+ }
17
+ it('foreground + live turn → nest (unchanged default)', () => {
18
+ expect(resolveSubagentStatusSurface(base)).toBe('nest')
19
+ })
20
+ it('THE fix: orphaned foreground (no live turn) → worker-feed', () => {
21
+ expect(resolveSubagentStatusSurface({ ...base, liveTurnPresent: false })).toBe('worker-feed')
22
+ })
23
+ it('kill switch: orphaned foreground with orphanStatus OFF → skip (pre-fix invisible)', () => {
24
+ expect(
25
+ resolveSubagentStatusSurface({ ...base, liveTurnPresent: false, orphanStatusEnabled: false }),
26
+ ).toBe('skip')
27
+ })
28
+ it('orphaned foreground but feed OFF → skip (nothing to surface through)', () => {
29
+ expect(
30
+ resolveSubagentStatusSurface({ ...base, liveTurnPresent: false, workerFeedEnabled: false }),
31
+ ).toBe('skip')
32
+ })
33
+ it('background + feed on → worker-feed', () => {
34
+ expect(resolveSubagentStatusSurface({ ...base, isBackground: true, liveTurnPresent: false })).toBe('worker-feed')
35
+ })
36
+ it('background + feed off → legacy-relay', () => {
37
+ expect(
38
+ resolveSubagentStatusSurface({ ...base, isBackground: true, workerFeedEnabled: false }),
39
+ ).toBe('legacy-relay')
40
+ })
41
+ })
42
+
43
+ describe('isOrphanSubagentStatusEnabled — default ON, =0 kill switch', () => {
44
+ it('undefined / "1" / "" → on; "0" → off', () => {
45
+ expect(isOrphanSubagentStatusEnabled(undefined)).toBe(true)
46
+ expect(isOrphanSubagentStatusEnabled('1')).toBe(true)
47
+ expect(isOrphanSubagentStatusEnabled('')).toBe(true)
48
+ expect(isOrphanSubagentStatusEnabled('0')).toBe(false)
49
+ })
50
+ })
51
+
52
+ // ── TOTAL-ENUMERATION DETERMINISM PROOF ─────────────────────────────────────
53
+ // 4 booleans = 16 reachable inputs. Enumerate all, assert totality, determinism,
54
+ // the documented table (independent spec), and the load-bearing invariants.
55
+ // (operator standard feedback_prove_finite_fsm_not_sample.)
56
+ function allInputs(): SubagentStatusSurfaceInput[] {
57
+ const rows: SubagentStatusSurfaceInput[] = []
58
+ for (const isBackground of [false, true])
59
+ for (const liveTurnPresent of [false, true])
60
+ for (const workerFeedEnabled of [false, true])
61
+ for (const orphanStatusEnabled of [false, true])
62
+ rows.push({ isBackground, liveTurnPresent, workerFeedEnabled, orphanStatusEnabled })
63
+ return rows
64
+ }
65
+
66
+ // Independent spec encoding (kept separate from the impl).
67
+ function spec(i: SubagentStatusSurfaceInput): SubagentStatusSurface {
68
+ if (i.isBackground) return i.workerFeedEnabled ? 'worker-feed' : 'legacy-relay'
69
+ if (i.liveTurnPresent) return 'nest'
70
+ if (!i.orphanStatusEnabled) return 'skip'
71
+ return i.workerFeedEnabled ? 'worker-feed' : 'skip'
72
+ }
73
+
74
+ describe('resolveSubagentStatusSurface — total enumeration (16 inputs)', () => {
75
+ const ROWS = allInputs()
76
+
77
+ it('exactly 16 reachable inputs (2^4)', () => {
78
+ expect(ROWS.length).toBe(16)
79
+ })
80
+ it('TOTAL + DETERMINISTIC: every input returns one of the four surfaces, idempotently', () => {
81
+ const surfaces = new Set<SubagentStatusSurface>(['nest', 'worker-feed', 'legacy-relay', 'skip'])
82
+ for (const i of ROWS) {
83
+ const a = resolveSubagentStatusSurface(i)
84
+ expect(surfaces.has(a)).toBe(true)
85
+ expect(resolveSubagentStatusSurface({ ...i })).toBe(a)
86
+ }
87
+ })
88
+ it('PRECEDENCE: matches the documented spec on all 16 inputs', () => {
89
+ for (const i of ROWS) expect(resolveSubagentStatusSurface(i)).toBe(spec(i))
90
+ })
91
+
92
+ it('INV-ORPHAN-VISIBLE: an orphaned foreground sub-agent is NEVER skip when orphanStatus + feed are on', () => {
93
+ for (const i of ROWS) {
94
+ if (!i.isBackground && !i.liveTurnPresent && i.orphanStatusEnabled && i.workerFeedEnabled) {
95
+ expect(resolveSubagentStatusSurface(i)).toBe('worker-feed')
96
+ }
97
+ }
98
+ })
99
+ it('INV-KILL-SWITCH: orphanStatus OFF ⇒ an orphaned foreground sub-agent is exactly the pre-fix behaviour (skip)', () => {
100
+ for (const i of ROWS) {
101
+ if (!i.isBackground && !i.liveTurnPresent && !i.orphanStatusEnabled) {
102
+ expect(resolveSubagentStatusSurface(i)).toBe('skip')
103
+ }
104
+ }
105
+ })
106
+ it('INV-NEST-UNCHANGED: a foreground sub-agent with a live turn is ALWAYS nest, independent of the other flags', () => {
107
+ for (const i of ROWS) {
108
+ if (!i.isBackground && i.liveTurnPresent) expect(resolveSubagentStatusSurface(i)).toBe('nest')
109
+ }
110
+ })
111
+ it('INV-BACKGROUND-UNCHANGED: background routing depends ONLY on the feed flag, never on liveTurn/orphanStatus', () => {
112
+ for (const i of ROWS) {
113
+ if (i.isBackground) {
114
+ expect(resolveSubagentStatusSurface(i)).toBe(i.workerFeedEnabled ? 'worker-feed' : 'legacy-relay')
115
+ }
116
+ }
117
+ })
118
+ })
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Where does a sub-agent's live status go?
3
+ *
4
+ * A sub-agent's progress is surfaced on one of four "surfaces". This pure
5
+ * decision picks which, so the routing is provable by total enumeration rather
6
+ * than buried in the gateway's imperative branches.
7
+ *
8
+ * - `nest` — a FOREGROUND sub-agent running inside a LIVE parent turn:
9
+ * its narrative nests under the parent's activity draft
10
+ * (the progress card). The default, unchanged.
11
+ * - `worker-feed` — a BACKGROUND worker (the `🛠 Worker` edit-in-place
12
+ * message), OR a FOREGROUND sub-agent that has NO live
13
+ * parent turn to nest into (dispatched outside a turn, or
14
+ * the turn ended while it kept running). The latter is the
15
+ * 2026-06-09 fix: extended autonomous work was invisible
16
+ * because foreground status was turn-scoped and a sub-agent
17
+ * with no turn silently returned. It now reuses the worker
18
+ * feed (with the same owner-DM fallback background workers
19
+ * already use), so post-turn work is always visible.
20
+ * - `legacy-relay` — a BACKGROUND worker when the worker feed is OFF: fall
21
+ * back to the legacy "still working" injected-inbound relay.
22
+ * - `skip` — nothing to surface (kill-switch off for an orphaned
23
+ * foreground, or no feed to surface it through).
24
+ *
25
+ * Determinism: the input space is 4 booleans = 16 rows, enumerated and proven
26
+ * in subagent-status-surface.test.ts (operator standard
27
+ * feedback_prove_finite_fsm_not_sample). The load-bearing invariant: an
28
+ * orphaned foreground sub-agent (no live turn) is `worker-feed`, never `skip`,
29
+ * whenever the orphan-status flag and the feed are both on.
30
+ */
31
+
32
+ export type SubagentStatusSurface = 'nest' | 'worker-feed' | 'legacy-relay' | 'skip'
33
+
34
+ export interface SubagentStatusSurfaceInput {
35
+ /** run_in_background dispatch (registry `subagents.background`). */
36
+ isBackground: boolean
37
+ /**
38
+ * A LIVE parent turn exists to nest this (foreground) sub-agent into.
39
+ * onProgress: `currentTurn != null`. onFinish: `currentTurn != null && it was
40
+ * actually nested` (so a foreground sub-agent that was surfaced via the worker
41
+ * feed — never nested — finalizes through the feed, not a turn collapse).
42
+ * Ignored for background sub-agents.
43
+ */
44
+ liveTurnPresent: boolean
45
+ /** SWITCHROOM_WORKER_ACTIVITY_FEED on. */
46
+ workerFeedEnabled: boolean
47
+ /** SWITCHROOM_ORPHAN_SUBAGENT_STATUS on — surface no-parent-turn foreground
48
+ * sub-agents via the worker feed. Off = pre-fix behaviour (invisible). */
49
+ orphanStatusEnabled: boolean
50
+ }
51
+
52
+ export function resolveSubagentStatusSurface(
53
+ input: SubagentStatusSurfaceInput,
54
+ ): SubagentStatusSurface {
55
+ if (!input.isBackground) {
56
+ // Foreground sub-agent.
57
+ if (input.liveTurnPresent) return 'nest'
58
+ // Orphaned foreground: no live turn to nest into — the invisible case.
59
+ if (!input.orphanStatusEnabled) return 'skip' // kill switch: pre-fix behaviour
60
+ return input.workerFeedEnabled ? 'worker-feed' : 'skip' // surfacing needs the feed
61
+ }
62
+ // Background worker.
63
+ return input.workerFeedEnabled ? 'worker-feed' : 'legacy-relay'
64
+ }
65
+
66
+ /** SWITCHROOM_ORPHAN_SUBAGENT_STATUS — default ON; `=0` restores pre-fix (invisible) behaviour. */
67
+ export function isOrphanSubagentStatusEnabled(envVal: string | undefined): boolean {
68
+ return envVal !== '0'
69
+ }