switchroom 0.12.24 → 0.12.26

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.
@@ -47247,8 +47247,8 @@ var {
47247
47247
  } = import__.default;
47248
47248
 
47249
47249
  // src/build-info.ts
47250
- var VERSION = "0.12.24";
47251
- var COMMIT_SHA = "7ab1329";
47250
+ var VERSION = "0.12.26";
47251
+ var COMMIT_SHA = "17486c7";
47252
47252
 
47253
47253
  // src/cli/agent.ts
47254
47254
  init_source();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.12.24",
3
+ "version": "0.12.26",
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": {
@@ -42961,10 +42961,12 @@ function createIpcServer(options) {
42961
42961
  const loggedLegacyUpdatePlaceholder = new Set;
42962
42962
  function removeClient(client3) {
42963
42963
  clients.delete(client3);
42964
- if (client3.agentName)
42964
+ if (client3.agentName && agentIndex.get(client3.agentName) === client3) {
42965
42965
  agentIndex.delete(client3.agentName);
42966
- if (client3.topicId != null)
42966
+ }
42967
+ if (client3.topicId != null && topicIndex.get(client3.topicId) === client3) {
42967
42968
  topicIndex.delete(client3.topicId);
42969
+ }
42968
42970
  loggedLegacyUpdatePlaceholder.delete(client3.id);
42969
42971
  onClientDisconnected(client3);
42970
42972
  log(`client disconnected: ${client3.id} (agent=${client3.agentName})`);
@@ -43070,6 +43072,13 @@ function createIpcServer(options) {
43070
43072
  agentIndex.delete(client3.agentName);
43071
43073
  if (client3.topicId != null)
43072
43074
  topicIndex.delete(client3.topicId);
43075
+ const existingClient = agentIndex.get(msg.agentName);
43076
+ if (existingClient && existingClient !== client3) {
43077
+ log(`register: closing prior client for agent=${msg.agentName} ` + `(prior_id=${existingClient.id} new_id=${client3.id}) \u2014 bridge reconnect race`);
43078
+ try {
43079
+ existingClient.close();
43080
+ } catch {}
43081
+ }
43073
43082
  client3.agentName = msg.agentName;
43074
43083
  client3.topicId = msg.topicId ?? null;
43075
43084
  agentIndex.set(msg.agentName, client3);
@@ -47357,11 +47366,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
47357
47366
  }
47358
47367
 
47359
47368
  // ../src/build-info.ts
47360
- var VERSION = "0.12.24";
47361
- var COMMIT_SHA = "7ab1329";
47362
- var COMMIT_DATE = "2026-05-20T14:48:10+10:00";
47363
- var LATEST_PR = 1582;
47364
- var COMMITS_AHEAD_OF_TAG = 3;
47369
+ var VERSION = "0.12.26";
47370
+ var COMMIT_SHA = "17486c7";
47371
+ var COMMIT_DATE = "2026-05-20T05:54:04Z";
47372
+ var LATEST_PR = 1586;
47373
+ var COMMITS_AHEAD_OF_TAG = 2;
47365
47374
 
47366
47375
  // gateway/boot-version.ts
47367
47376
  function formatRelativeAgo(iso) {
@@ -49234,7 +49243,9 @@ var ipcServer = createIpcServer({
49234
49243
  onClientRegistered(client3) {
49235
49244
  process.stderr.write(`telegram gateway: bridge registered \u2014 agent=${client3.agentName}
49236
49245
  `);
49237
- shadowEmit({ kind: "bridgeUp", at: Date.now() });
49246
+ if (client3.agentName != null) {
49247
+ shadowEmit({ kind: "bridgeUp", at: Date.now() });
49248
+ }
49238
49249
  client3.send({ type: "status", status: "agent_connected" });
49239
49250
  if (client3.agentName != null) {
49240
49251
  const pending = pendingInboundBuffer.drain(client3.agentName);
@@ -49328,7 +49339,9 @@ var ipcServer = createIpcServer({
49328
49339
  onClientDisconnected(client3) {
49329
49340
  process.stderr.write(`telegram gateway: bridge disconnected \u2014 agent=${client3.agentName}
49330
49341
  `);
49331
- shadowEmit({ kind: "bridgeDown", at: Date.now() });
49342
+ if (client3.agentName != null) {
49343
+ shadowEmit({ kind: "bridgeDown", at: Date.now() });
49344
+ }
49332
49345
  flushOnAgentDisconnect({
49333
49346
  agentName: client3.agentName,
49334
49347
  activeStatusReactions,
@@ -3196,8 +3196,17 @@ const ipcServer: IpcServer = createIpcServer({
3196
3196
 
3197
3197
  onClientRegistered(client: IpcClient) {
3198
3198
  process.stderr.write(`telegram gateway: bridge registered — agent=${client.agentName}\n`)
3199
- // Phase 2b shadow: bridge up.
3200
- shadowEmit({ kind: 'bridgeUp', at: Date.now() })
3199
+ // Phase 2b shadow: ONLY emit bridgeUp for the REAL bridge sidecar
3200
+ // (with an agent name). Anonymous IPC clients (recall.py, mcp
3201
+ // handshakes, etc.) connect briefly without a name and would
3202
+ // false-positive a bridgeUp/bridgeDown cycle that doesn't reflect
3203
+ // the real bridge state. This bug — discovered post-v0.12.24 — was
3204
+ // causing the shadow state to read `bridge_dead` even when the
3205
+ // real bridge was healthy, because every recall.py connect+disconnect
3206
+ // would flip the state.
3207
+ if (client.agentName != null) {
3208
+ shadowEmit({ kind: 'bridgeUp', at: Date.now() })
3209
+ }
3201
3210
  client.send({ type: 'status', status: 'agent_connected' })
3202
3211
 
3203
3212
  // #1150: drain any synthetic inbounds queued for this agent while
@@ -3323,8 +3332,12 @@ const ipcServer: IpcServer = createIpcServer({
3323
3332
 
3324
3333
  onClientDisconnected(client: IpcClient) {
3325
3334
  process.stderr.write(`telegram gateway: bridge disconnected — agent=${client.agentName}\n`)
3326
- // Phase 2b shadow: bridge down.
3327
- shadowEmit({ kind: 'bridgeDown', at: Date.now() })
3335
+ // Phase 2b shadow: ONLY emit bridgeDown for the REAL bridge sidecar
3336
+ // (matching the bridgeUp gate above). Anonymous IPC clients
3337
+ // disconnect frequently — those are not bridge flaps.
3338
+ if (client.agentName != null) {
3339
+ shadowEmit({ kind: 'bridgeDown', at: Date.now() })
3340
+ }
3328
3341
 
3329
3342
  // Scope the flush to clients that actually registered as an agent.
3330
3343
  // Anonymous one-shot connections (e.g. recall.py's legacy
@@ -266,8 +266,24 @@ export function createIpcServer(options: IpcServerOptions): IpcServer {
266
266
 
267
267
  function removeClient(client: IpcClient & { _socket: ReturnType<typeof Bun.listen> extends infer S ? any : never }) {
268
268
  clients.delete(client);
269
- if (client.agentName) agentIndex.delete(client.agentName);
270
- if (client.topicId != null) topicIndex.delete(client.topicId);
269
+ // CRITICAL race fix (2026-05-20): only delete from agentIndex /
270
+ // topicIndex if the index still points to THIS client. A bridge
271
+ // that reconnects fast can have its NEW client overwrite
272
+ // agentIndex[name] (via handleRegister's replace-not-reject)
273
+ // BEFORE the OLD client's close+removeClient runs. Blindly
274
+ // deleting agentIndex[name] would remove the LIVE replacement
275
+ // client by accident → sendToAgent returns false → all subsequent
276
+ // inbound buffered until the bridge happens to reconnect in an
277
+ // ordering that works out. User-visible symptom was the chronic
278
+ // bridge-flap pattern (clerk + gymbro unresponsive 2026-05-20)
279
+ // where the gateway log showed "bridge registered" but messages
280
+ // were still getting buffered as if no bridge existed.
281
+ if (client.agentName && agentIndex.get(client.agentName) === client) {
282
+ agentIndex.delete(client.agentName);
283
+ }
284
+ if (client.topicId != null && topicIndex.get(client.topicId) === client) {
285
+ topicIndex.delete(client.topicId);
286
+ }
271
287
  loggedLegacyUpdatePlaceholder.delete(client.id);
272
288
  onClientDisconnected(client);
273
289
  log(`client disconnected: ${client.id} (agent=${client.agentName})`);
@@ -407,6 +423,28 @@ export function createIpcServer(options: IpcServerOptions): IpcServer {
407
423
  if (client.agentName) agentIndex.delete(client.agentName);
408
424
  if (client.topicId != null) topicIndex.delete(client.topicId);
409
425
 
426
+ // 2026-05-20 race fix: if a PRIOR client is registered as this
427
+ // agent name (a stale/zombie connection that hasn't been evicted
428
+ // yet), explicitly close it before installing this new client.
429
+ // Without this, the prior client remains in `clients` set, its
430
+ // heartbeat watchdog still ticks, and its eventual close+removeClient
431
+ // can confuse routing. The removeClient identity-check fix above
432
+ // means the index won't be wrongly deleted, but two concurrent
433
+ // clients claiming the same agent name is still a routing hazard
434
+ // — close the zombie cleanly here.
435
+ const existingClient = agentIndex.get(msg.agentName);
436
+ if (existingClient && existingClient !== client) {
437
+ log(
438
+ `register: closing prior client for agent=${msg.agentName} ` +
439
+ `(prior_id=${existingClient.id} new_id=${client.id}) — bridge reconnect race`,
440
+ );
441
+ try {
442
+ (existingClient as IpcClientImpl).close();
443
+ } catch {
444
+ /* nothing to do */
445
+ }
446
+ }
447
+
410
448
  client.agentName = msg.agentName;
411
449
  client.topicId = msg.topicId ?? null;
412
450