switchroom 0.17.3 → 0.17.5

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.
@@ -63737,8 +63737,8 @@ import { existsSync, readFileSync } from "node:fs";
63737
63737
  import { dirname, join } from "node:path";
63738
63738
 
63739
63739
  // src/build-info.ts
63740
- var VERSION = "0.17.3";
63741
- var COMMIT_SHA = "b8f8edaa";
63740
+ var VERSION = "0.17.5";
63741
+ var COMMIT_SHA = "aae03307";
63742
63742
 
63743
63743
  // src/cli/resolve-version.ts
63744
63744
  function readPackageVersion() {
@@ -22670,7 +22670,7 @@ import { existsSync as existsSync6, readFileSync as readFileSync4 } from "node:f
22670
22670
  import { dirname as dirname4, join as join2 } from "node:path";
22671
22671
 
22672
22672
  // src/build-info.ts
22673
- var VERSION = "0.17.3";
22673
+ var VERSION = "0.17.5";
22674
22674
 
22675
22675
  // src/cli/resolve-version.ts
22676
22676
  function readPackageVersion() {
@@ -24819,7 +24819,7 @@ class HostdServer {
24819
24819
  });
24820
24820
  return this.reconcileFailedRolledBack(`write failed: ${e.message}`, req, caller, started);
24821
24821
  }
24822
- const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(["apply"]));
24822
+ const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(caller.kind === "agent" ? ["apply", "--only", callerName, "--non-interactive"] : ["apply", "--non-interactive"]));
24823
24823
  const recRes = await runner({ requestId: approvalId });
24824
24824
  if (recRes.exit_code === 0) {
24825
24825
  const blast = classifyBlastRadius(snapshot, postApply);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "switchroom",
3
3
  "//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
4
- "version": "0.17.3",
4
+ "version": "0.17.5",
5
5
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -303,12 +303,26 @@ for _stray_claude in \
303
303
  "$HOME/.local/bin/claude" \
304
304
  "$HOME/bin/claude"; do
305
305
  if [ -e "$_stray_claude" ] || [ -L "$_stray_claude" ]; then
306
+ # Skip our own canonical symlink re-seated at end of this block — it points
307
+ # back to the image binary and is not a shadow install. Any other file or
308
+ # symlink at this path (e.g. npm self-install, manual copy) is pruned.
309
+ if [ -L "$_stray_claude" ] && [ "$(readlink "$_stray_claude" 2>/dev/null)" = "/usr/local/bin/claude" ]; then
310
+ continue
311
+ fi
306
312
  echo "start.sh: pruning user-local claude shadow at $_stray_claude (image binary is authoritative)" >&2
307
313
  rm -f "$_stray_claude"
308
314
  fi
309
315
  done
310
316
  rm -rf "$HOME/.npm-global/lib/node_modules/@anthropic-ai/claude-code" 2>/dev/null || true
311
317
  unset _stray_claude
318
+ # Re-seat the canonical symlink so claude v2.1.x+ self-check ("claude command
319
+ # at $HOME/.local/bin/claude missing or broken") stays silent. The prune loop
320
+ # above skips this canonical pointer (via the readlink guard) and removes only
321
+ # genuine shadow installs; the ln -sf below creates the symlink on first boot
322
+ # and is a no-op on subsequent boots. `mkdir -p` is safe: the dir already exists
323
+ # or is about to be created.
324
+ mkdir -p "$HOME/.local/bin"
325
+ ln -sf /usr/local/bin/claude "$HOME/.local/bin/claude"
312
326
 
313
327
  # ── Root-tier agent: provision the docker CLI ────────────────────────
314
328
  # The root debugging agent (`root: true`) has /var/run/docker.sock
@@ -58590,10 +58590,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
58590
58590
  }
58591
58591
 
58592
58592
  // ../src/build-info.ts
58593
- var VERSION = "0.17.3";
58594
- var COMMIT_SHA = "b8f8edaa";
58595
- var COMMIT_DATE = "2026-07-05T10:15:33Z";
58596
- var LATEST_PR = 2836;
58593
+ var VERSION = "0.17.5";
58594
+ var COMMIT_SHA = "aae03307";
58595
+ var COMMIT_DATE = "2026-07-05T22:09:14+10:00";
58596
+ var LATEST_PR = null;
58597
58597
  var COMMITS_AHEAD_OF_TAG = 0;
58598
58598
 
58599
58599
  // gateway/boot-version.ts
@@ -60650,9 +60650,10 @@ var FEED_LIVENESS_OPEN_MS = (() => {
60650
60650
  })();
60651
60651
  var POST_ANSWER_LIVENESS_STALE_MS = parsePostAnswerLivenessMs(process.env.SWITCHROOM_POST_ANSWER_LIVENESS_STALE_MS) || 30000;
60652
60652
  function turnInFlightForGate() {
60653
+ const hasPendingApproval = pendingPermissions.size > 0;
60653
60654
  if (!isDeliveryCutoverEnabled())
60654
- return claudeBusyKeys.size > 0;
60655
- return probeGateParity(isMachineInTurn(), claudeBusyKeys.size);
60655
+ return claudeBusyKeys.size > 0 || hasPendingApproval;
60656
+ return probeGateParity(isMachineInTurn(), claudeBusyKeys.size) || hasPendingApproval;
60656
60657
  }
60657
60658
  function deliverResumeSyntheticOrBuffer(agent, inbound) {
60658
60659
  const decision = decideInboundDelivery({
@@ -2039,14 +2039,25 @@ const POST_ANSWER_LIVENESS_STALE_MS = parsePostAnswerLivenessMs(
2039
2039
  * message self-blocks. See the snapshot at the inbound handler.
2040
2040
  */
2041
2041
  function turnInFlightForGate(): boolean {
2042
- if (!isDeliveryCutoverEnabled()) return claudeBusyKeys.size > 0
2042
+ // Include pendingPermissions in the "turn busy" signal (#2841): after the
2043
+ // first interim reply, `releaseTurnBufferGate` clears claudeBusyKeys/machine
2044
+ // even though a permission card is still outstanding and claude is still
2045
+ // blocked mid-turn waiting for the tap verdict. A new inbound that arrives
2046
+ // in this window would see turnInFlightAtReceipt=false and deliver directly,
2047
+ // landing while claude's bridge is suspended in the permission-notification
2048
+ // handler — displacing the approval and orphaning the pending brevo/MCP call.
2049
+ // Keeping the gate closed for as long as there is an outstanding approval
2050
+ // card prevents that race. The gate reopens when the card is tapped or times
2051
+ // out (pendingPermissions.delete in finalizeCallback / TTL sweep).
2052
+ const hasPendingApproval = pendingPermissions.size > 0
2053
+ if (!isDeliveryCutoverEnabled()) return claudeBusyKeys.size > 0 || hasPendingApproval
2043
2054
  // Machine is authoritative. Run the log-only drift canary (#2794): the
2044
2055
  // imperative `claudeBusyKeys` shadow is still live in parallel, so a
2045
2056
  // dangerous over-hold divergence (machine holds the gate while the
2046
2057
  // imperative view is idle) is surfaced without changing behaviour. The
2047
2058
  // benign orphan-dangle direction — the wedge the machine self-heals — is
2048
2059
  // NOT flagged. `probeGateParity` returns the machine value unchanged.
2049
- return probeGateParity(isMachineInTurn(), claudeBusyKeys.size)
2060
+ return probeGateParity(isMachineInTurn(), claudeBusyKeys.size) || hasPendingApproval
2050
2061
  }
2051
2062
 
2052
2063
  /**
@@ -84,3 +84,29 @@ describe('no-repeat-on-timeout wiring', () => {
84
84
  expect(GATEWAY).toMatch(/permissionTimeoutSignatures\.delete\(sig\)/)
85
85
  })
86
86
  })
87
+
88
+ describe('inbound gate holds while approval card is outstanding (#2841)', () => {
89
+ // turnInFlightForGate() must return true when pendingPermissions is non-empty,
90
+ // even after releaseTurnBufferGate has cleared claudeBusyKeys/machine on the
91
+ // first interim reply. Without this, a new inbound delivered in that window
92
+ // displaces the approval context and orphans the pending MCP call.
93
+ it('turnInFlightForGate includes pendingPermissions.size > 0 on the legacy path', () => {
94
+ // span 1500: the function has a ~800-char comment block before the code lines.
95
+ const fn = slice(GATEWAY, 'function turnInFlightForGate()', 1500)
96
+ // Both paths (legacy claudeBusyKeys + machine-authoritative) must include the check.
97
+ expect(fn).toMatch(/claudeBusyKeys\.size\s*>\s*0\s*\|\|\s*hasPendingApproval/)
98
+ })
99
+
100
+ it('turnInFlightForGate includes pendingPermissions.size > 0 on the machine path', () => {
101
+ const fn = slice(GATEWAY, 'function turnInFlightForGate()', 1600)
102
+ // probeGateParity(...) || hasPendingApproval — the inner arg contains its own
103
+ // parens (isMachineInTurn()), so match the two tokens independently.
104
+ expect(fn).toContain('probeGateParity(')
105
+ expect(fn).toMatch(/probeGateParity[\s\S]+?\|\|\s*hasPendingApproval/)
106
+ })
107
+
108
+ it('hasPendingApproval reads pendingPermissions.size', () => {
109
+ const fn = slice(GATEWAY, 'function turnInFlightForGate()', 1500)
110
+ expect(fn).toMatch(/pendingPermissions\.size\s*>\s*0/)
111
+ })
112
+ })