switchroom 0.14.56 → 0.14.57

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.
@@ -49463,8 +49463,8 @@ var {
49463
49463
  } = import__.default;
49464
49464
 
49465
49465
  // src/build-info.ts
49466
- var VERSION = "0.14.56";
49467
- var COMMIT_SHA = "a9b8b5ae";
49466
+ var VERSION = "0.14.57";
49467
+ var COMMIT_SHA = "ddb0b353";
49468
49468
 
49469
49469
  // src/cli/agent.ts
49470
49470
  init_source();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.56",
3
+ "version": "0.14.57",
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": {
@@ -52195,10 +52195,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52195
52195
  }
52196
52196
 
52197
52197
  // ../src/build-info.ts
52198
- var VERSION = "0.14.56";
52199
- var COMMIT_SHA = "a9b8b5ae";
52200
- var COMMIT_DATE = "2026-06-03T21:45:00Z";
52201
- var LATEST_PR = 2138;
52198
+ var VERSION = "0.14.57";
52199
+ var COMMIT_SHA = "ddb0b353";
52200
+ var COMMIT_DATE = "2026-06-03T22:37:37Z";
52201
+ var LATEST_PR = 2140;
52202
52202
  var COMMITS_AHEAD_OF_TAG = 0;
52203
52203
 
52204
52204
  // gateway/boot-version.ts
@@ -57090,7 +57090,7 @@ async function drainActivitySummary(turn) {
57090
57090
  turn.activityInFlight = null;
57091
57091
  }
57092
57092
  }
57093
- function clearActivitySummary(turn) {
57093
+ function clearActivitySummary(turn, finalHtmlOverride) {
57094
57094
  const chat = turn.sessionChatId;
57095
57095
  const thread = turn.sessionThreadId;
57096
57096
  const inFlight = turn.activityInFlight ?? Promise.resolve();
@@ -57108,7 +57108,7 @@ function clearActivitySummary(turn) {
57108
57108
  }
57109
57109
  return;
57110
57110
  }
57111
- const finalHtml = composeTurnActivity(turn, true);
57111
+ const finalHtml = finalHtmlOverride !== undefined ? finalHtmlOverride : composeTurnActivity(turn, true);
57112
57112
  if (finalHtml == null)
57113
57113
  return;
57114
57114
  try {
@@ -63416,16 +63416,18 @@ var didOneTimeSetup = false;
63416
63416
  const isBackground = dispatch.isBackground;
63417
63417
  if (!isBackground) {
63418
63418
  const turn = currentTurn;
63419
- const removed = turn != null && turn.foregroundSubAgents.delete(agentId);
63420
- if (turn != null && removed) {
63419
+ if (turn != null && turn.foregroundSubAgents.has(agentId)) {
63421
63420
  const action = foregroundFinishAction({
63422
- removed,
63421
+ removed: true,
63423
63422
  replyCalled: turn.replyCalled,
63424
- remainingForeground: turn.foregroundSubAgents.size
63423
+ remainingForeground: turn.foregroundSubAgents.size - 1
63425
63424
  });
63426
63425
  if (action === "handoff-clear") {
63427
- clearActivitySummary(turn);
63426
+ const finalHtml = composeTurnActivity(turn, true);
63427
+ turn.foregroundSubAgents.delete(agentId);
63428
+ clearActivitySummary(turn, finalHtml);
63428
63429
  } else if (action === "recompose") {
63430
+ turn.foregroundSubAgents.delete(agentId);
63429
63431
  const rendered = composeTurnActivity(turn);
63430
63432
  if (rendered != null) {
63431
63433
  turn.activityPendingRender = rendered;
@@ -8467,8 +8467,18 @@ async function drainActivitySummary(turn: CurrentTurn): Promise<void> {
8467
8467
  * Called on the first reply (hand-off) and again at turn_end (no-reply safety
8468
8468
  * net); finalize edits are idempotent (a 'message is not modified' on the
8469
8469
  * second call is swallowed).
8470
+ *
8471
+ * `finalHtmlOverride` (finalize path only): a render captured by the caller
8472
+ * BEFORE it tore down turn state the finalize render depends on. The
8473
+ * foreground handoff-clear path passes this — it deletes the just-finished
8474
+ * sub-agent's narrative right after this call, so the async
8475
+ * `composeTurnActivity(turn, true)` below would see an emptied feed (and, on
8476
+ * ack-first turns, empty `mirrorLines`), render null, and skip the finalize —
8477
+ * freezing the last live "→ in-progress" line. The captured render keeps the
8478
+ * persisted record reading done (✓). Omitted → compute it here (the common
8479
+ * reply/turn_end callers, where state is stable).
8470
8480
  */
8471
- function clearActivitySummary(turn: CurrentTurn): void {
8481
+ function clearActivitySummary(turn: CurrentTurn, finalHtmlOverride?: string | null): void {
8472
8482
  const chat = turn.sessionChatId
8473
8483
  const thread = turn.sessionThreadId
8474
8484
  const inFlight = turn.activityInFlight ?? Promise.resolve()
@@ -8489,7 +8499,8 @@ function clearActivitySummary(turn: CurrentTurn): void {
8489
8499
  }
8490
8500
  // Default: leave the status message as a record, edited to a terminal
8491
8501
  // all-done state so it doesn't freeze on a misleading "→ in-progress" line.
8492
- const finalHtml = composeTurnActivity(turn, true)
8502
+ const finalHtml =
8503
+ finalHtmlOverride !== undefined ? finalHtmlOverride : composeTurnActivity(turn, true)
8493
8504
  if (finalHtml == null) return
8494
8505
  try {
8495
8506
  await robustApiCall(
@@ -19278,20 +19289,32 @@ void (async () => {
19278
19289
  // tool result, so there's no handback to deliver. Reaction
19279
19290
  // promotion already ran above.
19280
19291
  const turn = currentTurn
19281
- const removed = turn != null && turn.foregroundSubAgents.delete(agentId)
19282
- if (turn != null && removed) {
19292
+ // has()-then-delete (not delete-up-front): the handoff-clear
19293
+ // branch must render the finished sub-agent's steps as done
19294
+ // WHILE its narrative is still in the map, then remove it.
19295
+ if (turn != null && turn.foregroundSubAgents.has(agentId)) {
19283
19296
  const action = foregroundFinishAction({
19284
- removed,
19297
+ removed: true,
19285
19298
  replyCalled: turn.replyCalled,
19286
- remainingForeground: turn.foregroundSubAgents.size,
19299
+ // size AFTER this agent's impending removal
19300
+ remainingForeground: turn.foregroundSubAgents.size - 1,
19287
19301
  })
19288
19302
  if (action === 'handoff-clear') {
19289
19303
  // Post-ack: the last foreground sub-agent finished and
19290
19304
  // the parent will now produce its answer inline. Hand
19291
19305
  // the re-opened feed off to the answer, mirroring the
19292
- // first-reply clear (turn_end is the safety net).
19293
- clearActivitySummary(turn)
19306
+ // first-reply clear (turn_end is the safety net). Capture
19307
+ // the finalized render (child steps done ✓) BEFORE the
19308
+ // delete, then pass it so the persisted record doesn't
19309
+ // freeze on a stale "→ in-progress" line (the emptied-feed
19310
+ // skip — see clearActivitySummary's finalHtmlOverride doc).
19311
+ const finalHtml = composeTurnActivity(turn, true)
19312
+ turn.foregroundSubAgents.delete(agentId)
19313
+ clearActivitySummary(turn, finalHtml)
19294
19314
  } else if (action === 'recompose') {
19315
+ // Collapse the finished sub-agent's block: delete first,
19316
+ // then render WITHOUT it (live feed keeps its → step).
19317
+ turn.foregroundSubAgents.delete(agentId)
19295
19318
  const rendered = composeTurnActivity(turn)
19296
19319
  if (rendered != null) {
19297
19320
  turn.activityPendingRender = rendered
@@ -222,4 +222,30 @@ describe("renderActivityFeedWithNested — foreground sub-agent nesting (Model A
222
222
  "<i>✓ Reading a.ts</i>",
223
223
  );
224
224
  });
225
+
226
+ // Pins the invariant the gateway's foreground handoff-clear path relies on:
227
+ // on an ack-first turn the parent feed is empty (mirrorLines=[]) and the only
228
+ // content is the foreground sub-agent's nested narrative. The finalized
229
+ // render MUST be captured WHILE that narrative is present — once the gateway
230
+ // removes the finished sub-agent from the map, the render collapses to null
231
+ // and the finalize would be skipped, freezing the last live "→" line. This is
232
+ // exactly why clearActivitySummary takes a pre-delete finalHtmlOverride.
233
+ describe("foreground handoff-clear: capture-before-delete invariant", () => {
234
+ it("ack-first (empty parent) + child present → non-null all-done render (✓, no →)", () => {
235
+ const out = renderActivityFeedWithNested(
236
+ [],
237
+ ["Sleep 2 for step 8", "Step 8 done; final echo", "All eight steps completed"],
238
+ true,
239
+ );
240
+ expect(out).not.toBeNull();
241
+ expect(out).not.toContain("→");
242
+ expect(out).toContain("All eight steps completed");
243
+ });
244
+
245
+ it("ack-first (empty parent) + child REMOVED → null (the emptied-feed skip the gateway must avoid)", () => {
246
+ // After foregroundSubAgents.delete(agentId), the parent has nothing left
247
+ // to render on an ack-first turn → null → finalize would no-op.
248
+ expect(renderActivityFeedWithNested([], [], true)).toBeNull();
249
+ });
250
+ });
225
251
  });