switchroom 0.14.16 โ†’ 0.14.17

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.
@@ -49413,8 +49413,8 @@ var {
49413
49413
  } = import__.default;
49414
49414
 
49415
49415
  // src/build-info.ts
49416
- var VERSION = "0.14.16";
49417
- var COMMIT_SHA = "6f5d9562";
49416
+ var VERSION = "0.14.17";
49417
+ var COMMIT_SHA = "95c1d475";
49418
49418
 
49419
49419
  // src/cli/agent.ts
49420
49420
  init_source();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.16",
3
+ "version": "0.14.17",
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": {
@@ -51140,10 +51140,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
51140
51140
  }
51141
51141
 
51142
51142
  // ../src/build-info.ts
51143
- var VERSION = "0.14.16";
51144
- var COMMIT_SHA = "6f5d9562";
51145
- var COMMIT_DATE = "2026-05-30T04:37:39Z";
51146
- var LATEST_PR = 2003;
51143
+ var VERSION = "0.14.17";
51144
+ var COMMIT_SHA = "95c1d475";
51145
+ var COMMIT_DATE = "2026-05-30T05:17:47Z";
51146
+ var LATEST_PR = 2005;
51147
51147
  var COMMITS_AHEAD_OF_TAG = 0;
51148
51148
 
51149
51149
  // gateway/boot-version.ts
@@ -51605,6 +51605,34 @@ function applySubagentsSchema(db2) {
51605
51605
  }
51606
51606
  db2.exec("CREATE INDEX IF NOT EXISTS subagents_jsonl_id ON subagents(jsonl_agent_id)");
51607
51607
  }
51608
+ function mapSubagentRow(row) {
51609
+ return {
51610
+ id: row.id,
51611
+ parent_session_id: row.parent_session_id,
51612
+ parent_turn_key: row.parent_turn_key,
51613
+ agent_type: row.agent_type,
51614
+ description: row.description,
51615
+ background: row.background !== 0,
51616
+ started_at: row.started_at,
51617
+ last_activity_at: row.last_activity_at,
51618
+ ended_at: row.ended_at,
51619
+ status: row.status,
51620
+ result_summary: row.result_summary,
51621
+ jsonl_agent_id: row.jsonl_agent_id
51622
+ };
51623
+ }
51624
+ function getSubagentByJsonlId(db2, jsonlAgentId) {
51625
+ const row = db2.prepare("SELECT * FROM subagents WHERE jsonl_agent_id = ?").get(jsonlAgentId);
51626
+ return row ? mapSubagentRow(row) : null;
51627
+ }
51628
+
51629
+ // gateway/worker-feed-dispatch.ts
51630
+ function resolveWorkerFeedDispatch(sub, watcherDescription) {
51631
+ return {
51632
+ isBackground: sub?.background ?? false,
51633
+ feedDescription: (sub?.description ?? "") || watcherDescription
51634
+ };
51635
+ }
51608
51636
 
51609
51637
  // gateway/resolve-calling-subagent.ts
51610
51638
  function resolveCallingSubagent(opts) {
@@ -61332,8 +61360,6 @@ var didOneTimeSetup = false;
61332
61360
  onFinish: ({ agentId, outcome, description, resultText, toolCount, durationMs }) => {
61333
61361
  deferredDoneReactions.promote();
61334
61362
  let fleetChatId = "";
61335
- let isBackground = false;
61336
- let dispatchDesc = "";
61337
61363
  try {
61338
61364
  const fleets = progressDriver?.peekAllFleets() ?? [];
61339
61365
  for (const f of fleets) {
@@ -61343,18 +61369,16 @@ var didOneTimeSetup = false;
61343
61369
  }
61344
61370
  }
61345
61371
  } catch {}
61372
+ let dispatch = resolveWorkerFeedDispatch(null, description);
61346
61373
  if (turnsDb != null) {
61347
61374
  try {
61348
- const row = turnsDb.prepare("SELECT background, description FROM subagents WHERE jsonl_agent_id = ?").get(agentId);
61349
- if (row != null) {
61350
- isBackground = row.background === 1;
61351
- dispatchDesc = row.description ?? "";
61352
- }
61375
+ dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId(turnsDb, agentId), description);
61353
61376
  } catch {}
61354
61377
  }
61378
+ const isBackground = dispatch.isBackground;
61355
61379
  if (workerFeedEnabled) {
61356
61380
  workerActivityFeed.finish(agentId, {
61357
- description: dispatchDesc || description,
61381
+ description: dispatch.feedDescription,
61358
61382
  lastTool: null,
61359
61383
  toolCount,
61360
61384
  latestSummary: resultText,
@@ -61396,8 +61420,6 @@ var didOneTimeSetup = false;
61396
61420
  },
61397
61421
  onProgress: ({ agentId, description, latestSummary, elapsedMs, prevBucketIdx, setBucketIdx, lastTool, toolCount }) => {
61398
61422
  let fleetChatId = "";
61399
- let isBackground = false;
61400
- let dispatchDesc = "";
61401
61423
  try {
61402
61424
  const fleets = progressDriver?.peekAllFleets() ?? [];
61403
61425
  for (const f of fleets) {
@@ -61407,20 +61429,18 @@ var didOneTimeSetup = false;
61407
61429
  }
61408
61430
  }
61409
61431
  } catch {}
61432
+ let dispatch = resolveWorkerFeedDispatch(null, description);
61410
61433
  if (turnsDb != null) {
61411
61434
  try {
61412
- const row = turnsDb.prepare("SELECT background, description FROM subagents WHERE jsonl_agent_id = ?").get(agentId);
61413
- if (row != null) {
61414
- isBackground = row.background === 1;
61415
- dispatchDesc = row.description ?? "";
61416
- }
61435
+ dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId(turnsDb, agentId), description);
61417
61436
  } catch {}
61418
61437
  }
61438
+ const isBackground = dispatch.isBackground;
61419
61439
  if (!isBackground)
61420
61440
  return;
61421
61441
  if (workerFeedEnabled) {
61422
61442
  workerActivityFeed.update(agentId, fleetChatId || (loadAccess().allowFrom[0] ?? ""), {
61423
- description: dispatchDesc || description,
61443
+ description: dispatch.feedDescription,
61424
61444
  lastTool,
61425
61445
  toolCount,
61426
61446
  latestSummary,
@@ -418,7 +418,8 @@ import {
418
418
  findMostRecentInterruptedTurn,
419
419
  findRecentTurnsForChat,
420
420
  } from '../registry/turns-schema.js'
421
- import { applySubagentsSchema } from '../registry/subagents-schema.js'
421
+ import { applySubagentsSchema, getSubagentByJsonlId } from '../registry/subagents-schema.js'
422
+ import { resolveWorkerFeedDispatch, type WorkerFeedDispatch } from './worker-feed-dispatch.js'
422
423
  import { formatIdleFooter } from '../idle-footer.js'
423
424
  import { resolveCallingSubagent } from './resolve-calling-subagent.js'
424
425
 
@@ -17402,11 +17403,6 @@ void (async () => {
17402
17403
  // independent of the gateway โ€” see
17403
17404
  // `subagent-handback-decision.test.ts`.
17404
17405
  let fleetChatId = ''
17405
- let isBackground = false
17406
- // Dispatch-time task description from the registry row โ€”
17407
- // see the onProgress note; used for the feed's terminal recap
17408
- // header so it matches the running header ("ยท <real task>").
17409
- let dispatchDesc = ''
17410
17406
  try {
17411
17407
  const fleets = progressDriver?.peekAllFleets() ?? []
17412
17408
  for (const f of fleets) {
@@ -17419,17 +17415,18 @@ void (async () => {
17419
17415
  // peek failures are non-fatal โ€” fall through to the
17420
17416
  // owner-chat fallback inside decideSubagentHandback.
17421
17417
  }
17418
+ // Background flag + feed header description, both derived from
17419
+ // the registry row via the pure resolveWorkerFeedDispatch
17420
+ // (worker-feed-dispatch.ts, pinned by its test). Best-effort:
17421
+ // a DB hiccup keeps the watcher's generic label rather than
17422
+ // throwing out of the terminal handler.
17423
+ let dispatch: WorkerFeedDispatch = resolveWorkerFeedDispatch(null, description)
17422
17424
  if (turnsDb != null) {
17423
17425
  try {
17424
- const row = turnsDb
17425
- .prepare('SELECT background, description FROM subagents WHERE jsonl_agent_id = ?')
17426
- .get(agentId) as { background: number; description: string | null } | undefined
17427
- if (row != null) {
17428
- isBackground = row.background === 1
17429
- dispatchDesc = row.description ?? ''
17430
- }
17426
+ dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId(turnsDb, agentId), description)
17431
17427
  } catch { /* best-effort */ }
17432
17428
  }
17429
+ const isBackground = dispatch.isBackground
17433
17430
  // #PR2 live worker-feed: force the terminal recap edit on
17434
17431
  // the worker's live message. No-op when no message was ever
17435
17432
  // posted (trivial workers stay silent; handback covers them).
@@ -17437,7 +17434,7 @@ void (async () => {
17437
17434
  // it to 'done' so an already-posted message still finalizes.
17438
17435
  if (workerFeedEnabled) {
17439
17436
  void workerActivityFeed.finish(agentId, {
17440
- description: dispatchDesc || description,
17437
+ description: dispatch.feedDescription,
17441
17438
  lastTool: null,
17442
17439
  toolCount,
17443
17440
  latestSummary: resultText,
@@ -17517,12 +17514,6 @@ void (async () => {
17517
17514
  // lives in the `onFinish` block just above.
17518
17515
  onProgress: ({ agentId, description, latestSummary, elapsedMs, prevBucketIdx, setBucketIdx, lastTool, toolCount }) => {
17519
17516
  let fleetChatId = ''
17520
- let isBackground = false
17521
- // The watcher's `description` is its 'sub-agent' default (it
17522
- // never reassigns it from the worker jsonl). The dispatch-time
17523
- // task description lives in the registry row โ€” prefer it so the
17524
- // feed header reads "๐Ÿ”ง Worker ยท <real task>" not "ยท sub-agent".
17525
- let dispatchDesc = ''
17526
17517
  try {
17527
17518
  const fleets = progressDriver?.peekAllFleets() ?? []
17528
17519
  for (const f of fleets) {
@@ -17532,17 +17523,18 @@ void (async () => {
17532
17523
  }
17533
17524
  }
17534
17525
  } catch { /* peek failures non-fatal */ }
17526
+ // The watcher's `description` is its 'sub-agent' default (it
17527
+ // never reassigns it from the worker jsonl). The dispatch-time
17528
+ // task description lives in the registry row โ€” resolveWorkerFeedDispatch
17529
+ // prefers it so the header reads "๐Ÿ”ง Worker ยท <real task>" not
17530
+ // "ยท sub-agent" (worker-feed-dispatch.ts, pinned by its test).
17531
+ let dispatch: WorkerFeedDispatch = resolveWorkerFeedDispatch(null, description)
17535
17532
  if (turnsDb != null) {
17536
17533
  try {
17537
- const row = turnsDb
17538
- .prepare('SELECT background, description FROM subagents WHERE jsonl_agent_id = ?')
17539
- .get(agentId) as { background: number; description: string | null } | undefined
17540
- if (row != null) {
17541
- isBackground = row.background === 1
17542
- dispatchDesc = row.description ?? ''
17543
- }
17534
+ dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId(turnsDb, agentId), description)
17544
17535
  } catch { /* best-effort */ }
17545
17536
  }
17537
+ const isBackground = dispatch.isBackground
17546
17538
  if (!isBackground) return // skip overhead for foreground
17547
17539
 
17548
17540
  // #PR2 live worker-feed: when ON, the worker's live chat
@@ -17556,7 +17548,7 @@ void (async () => {
17556
17548
  agentId,
17557
17549
  fleetChatId || (loadAccess().allowFrom[0] ?? ''),
17558
17550
  {
17559
- description: dispatchDesc || description,
17551
+ description: dispatch.feedDescription,
17560
17552
  lastTool,
17561
17553
  toolCount,
17562
17554
  latestSummary,
@@ -0,0 +1,37 @@
1
+ import type { Subagent } from '../registry/subagents-schema.js'
2
+
3
+ export interface WorkerFeedDispatch {
4
+ /** True when the sub-agent was dispatched with `run_in_background: true`. */
5
+ isBackground: boolean
6
+ /**
7
+ * The human-readable task to render in the feed header
8
+ * ("๐Ÿ”ง Worker ยท <feedDescription>").
9
+ */
10
+ feedDescription: string
11
+ }
12
+
13
+ /**
14
+ * Resolve the two registry-derived inputs the worker-activity feed needs:
15
+ * whether the sub-agent was a background dispatch, and the task description
16
+ * to show in the feed header.
17
+ *
18
+ * The live watcher only carries a generic 'sub-agent' label โ€” it never
19
+ * reassigns `description` from the worker jsonl. The real dispatch-time
20
+ * description lives in the registry `subagents` row (written by the pretool
21
+ * hook from the `Agent(description:)` input). Prefer it; fall back to the
22
+ * watcher's label only when the row is missing or its description is empty.
23
+ *
24
+ * Pure + DB-free so it pins the #2002 behavior under both vitest and bun โ€”
25
+ * see worker-feed-dispatch.test.ts. The gateway must never inline this
26
+ * decision again: a regression here silently reverts the feed header to
27
+ * "ยท sub-agent".
28
+ */
29
+ export function resolveWorkerFeedDispatch(
30
+ sub: Subagent | null,
31
+ watcherDescription: string,
32
+ ): WorkerFeedDispatch {
33
+ return {
34
+ isBackground: sub?.background ?? false,
35
+ feedDescription: (sub?.description ?? '') || watcherDescription,
36
+ }
37
+ }
@@ -73,7 +73,13 @@ export interface WorkerEntry {
73
73
  readonly agentId: string
74
74
  /** File path of the JSONL. */
75
75
  readonly filePath: string
76
- /** Short description โ€” from the sub-agent's first text/narrative line. */
76
+ /**
77
+ * Generic 'sub-agent' placeholder โ€” the watcher deliberately does NOT
78
+ * reassign this from the worker jsonl (see the init at construction and
79
+ * the "Do NOT overwrite" note in the line-handler). The real dispatch-time
80
+ * task description lives in the registry `subagents` row; the gateway reads
81
+ * it there via resolveWorkerFeedDispatch for the worker-feed header.
82
+ */
77
83
  description: string
78
84
  /** Current lifecycle state. */
79
85
  state: WorkerState
@@ -864,6 +870,9 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
864
870
  const entry: WorkerEntry = {
865
871
  agentId,
866
872
  filePath,
873
+ // Generic placeholder only โ€” never overwritten from the jsonl. The
874
+ // gateway substitutes the real registry description for the worker
875
+ // feed (resolveWorkerFeedDispatch). See the WorkerEntry.description doc.
867
876
  description: 'sub-agent',
868
877
  state: 'running',
869
878
  dispatchedAt: n,
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { Subagent } from '../registry/subagents-schema.js'
3
+ import { resolveWorkerFeedDispatch } from '../gateway/worker-feed-dispatch.js'
4
+
5
+ function makeSub(over: Partial<Subagent>): Subagent {
6
+ return {
7
+ id: 'toolu_01ABC',
8
+ parent_session_id: null,
9
+ parent_turn_key: null,
10
+ agent_type: 'general-purpose',
11
+ description: null,
12
+ background: false,
13
+ started_at: 0,
14
+ last_activity_at: null,
15
+ ended_at: null,
16
+ status: 'running',
17
+ result_summary: null,
18
+ jsonl_agent_id: 'a37ad7639ae61476c',
19
+ ...over,
20
+ }
21
+ }
22
+
23
+ describe('resolveWorkerFeedDispatch (#2002 regression pin)', () => {
24
+ it('uses the real registry description for the feed header, not the watcher label', () => {
25
+ const sub = makeSub({ background: true, description: 'Background ten-step worker' })
26
+ const out = resolveWorkerFeedDispatch(sub, 'sub-agent')
27
+ expect(out.isBackground).toBe(true)
28
+ expect(out.feedDescription).toBe('Background ten-step worker')
29
+ })
30
+
31
+ it('falls back to the watcher label when the registry row is missing', () => {
32
+ const out = resolveWorkerFeedDispatch(null, 'sub-agent')
33
+ expect(out.isBackground).toBe(false)
34
+ expect(out.feedDescription).toBe('sub-agent')
35
+ })
36
+
37
+ it('falls back to the watcher label when the registry description is null', () => {
38
+ const sub = makeSub({ background: true, description: null })
39
+ const out = resolveWorkerFeedDispatch(sub, 'sub-agent')
40
+ expect(out.isBackground).toBe(true)
41
+ expect(out.feedDescription).toBe('sub-agent')
42
+ })
43
+
44
+ it('falls back to the watcher label when the registry description is empty', () => {
45
+ const sub = makeSub({ background: true, description: '' })
46
+ const out = resolveWorkerFeedDispatch(sub, 'sub-agent')
47
+ expect(out.feedDescription).toBe('sub-agent')
48
+ })
49
+
50
+ it('reports a foreground sub-agent as not background', () => {
51
+ const sub = makeSub({ background: false, description: 'inline helper' })
52
+ const out = resolveWorkerFeedDispatch(sub, 'sub-agent')
53
+ expect(out.isBackground).toBe(false)
54
+ // description still resolves โ€” callers gate on isBackground separately.
55
+ expect(out.feedDescription).toBe('inline helper')
56
+ })
57
+
58
+ it('a missing row defaults isBackground false so the feed never fires blind', () => {
59
+ // The gateway gates the feed on isBackground; a registry miss must not
60
+ // flip a foreground turn into a background one.
61
+ expect(resolveWorkerFeedDispatch(null, '').isBackground).toBe(false)
62
+ })
63
+ })