switchroom 0.13.25 → 0.13.27

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.
Files changed (31) hide show
  1. package/dist/cli/switchroom.js +132 -10
  2. package/dist/vault/broker/server.js +32 -4
  3. package/package.json +1 -1
  4. package/telegram-plugin/active-reactions-sweep.ts +4 -4
  5. package/telegram-plugin/dist/gateway/gateway.js +239 -64
  6. package/telegram-plugin/docs/waiting-ux-spec.md +17 -1
  7. package/telegram-plugin/gateway/disconnect-flush.ts +10 -6
  8. package/telegram-plugin/gateway/gateway.ts +166 -51
  9. package/telegram-plugin/gateway/inbound-spool.ts +69 -2
  10. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +14 -0
  11. package/telegram-plugin/gateway/subagent-progress-inbound-builder.ts +256 -0
  12. package/telegram-plugin/pending-work-progress.ts +5 -1
  13. package/telegram-plugin/status-reactions.ts +70 -58
  14. package/telegram-plugin/stream-reply-handler.ts +7 -36
  15. package/telegram-plugin/subagent-watcher.ts +64 -3
  16. package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -3
  17. package/telegram-plugin/tests/inbound-spool-progress.test.ts +213 -0
  18. package/telegram-plugin/tests/inbound-spool.test.ts +62 -0
  19. package/telegram-plugin/tests/multi-turn-continuity.test.ts +0 -1
  20. package/telegram-plugin/tests/outbound-ordering.test.ts +0 -1
  21. package/telegram-plugin/tests/parse-mode-rotation.test.ts +0 -1
  22. package/telegram-plugin/tests/reply-terminal-reaction.test.ts +78 -135
  23. package/telegram-plugin/tests/status-accent.test.ts +0 -1
  24. package/telegram-plugin/tests/status-reactions.test.ts +56 -27
  25. package/telegram-plugin/tests/stream-reply-error-paths.test.ts +0 -1
  26. package/telegram-plugin/tests/stream-reply-handler.test.ts +9 -25
  27. package/telegram-plugin/tests/streaming-e2e.test.ts +0 -1
  28. package/telegram-plugin/tests/streaming-orchestration.test.ts +0 -1
  29. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +22 -0
  30. package/telegram-plugin/tests/subagent-progress-inbound-builder.test.ts +269 -0
  31. package/telegram-plugin/uat/scenarios/jtbd-reflective-status-reaction-dm.test.ts +204 -0
@@ -29405,6 +29405,7 @@ __export(exports_doctor, {
29405
29405
  tryReadHostFile: () => tryReadHostFile,
29406
29406
  telegramGetMe: () => telegramGetMe,
29407
29407
  registerDoctorCommand: () => registerDoctorCommand,
29408
+ probeVaultBrokerSocketPair: () => probeVaultBrokerSocketPair,
29408
29409
  printSection: () => printSection,
29409
29410
  parseSimpleEnv: () => parseSimpleEnv,
29410
29411
  parsePythonVersion: () => parsePythonVersion,
@@ -29417,6 +29418,7 @@ __export(exports_doctor, {
29417
29418
  findChromium: () => findChromium,
29418
29419
  deriveEd25519PublicKeyBytes: () => deriveEd25519PublicKeyBytes,
29419
29420
  classifyReadError: () => classifyReadError,
29421
+ checkVaultBrokerSocketPairs: () => checkVaultBrokerSocketPairs,
29420
29422
  checkTelegram: () => checkTelegram,
29421
29423
  checkStartShStale: () => checkStartShStale,
29422
29424
  checkSkillsPrerequisites: () => checkSkillsPrerequisites,
@@ -29742,6 +29744,93 @@ function checkLegacyState() {
29742
29744
  }
29743
29745
  return results;
29744
29746
  }
29747
+ function probeVaultBrokerSocketPair(agentName) {
29748
+ const dataPath = `/run/switchroom/broker/${agentName}/sock`;
29749
+ const unlockPath = `/run/switchroom/broker/${agentName}/unlock`;
29750
+ const script = `D=0; U=0; ` + `test -S '${dataPath}' && D=1; ` + `test -S '${unlockPath}' && U=1; ` + `echo "D=$D U=$U"`;
29751
+ const r = spawnSync7("docker", ["exec", "switchroom-vault-broker", "sh", "-c", script], { stdio: "pipe", timeout: 3000 });
29752
+ if (r.error || r.status === null)
29753
+ return "unreachable";
29754
+ if (r.status !== 0) {
29755
+ if (r.status >= 125)
29756
+ return "unreachable";
29757
+ return "unreachable";
29758
+ }
29759
+ const out = r.stdout.toString();
29760
+ const dOk = /D=1/.test(out);
29761
+ const uOk = /U=1/.test(out);
29762
+ if (dOk && uOk)
29763
+ return "ok";
29764
+ if (dOk && !uOk)
29765
+ return "missing-unlock";
29766
+ if (!dOk && uOk)
29767
+ return "missing-data";
29768
+ return "missing-both";
29769
+ }
29770
+ function checkVaultBrokerSocketPairs(config, opts) {
29771
+ const agentNames = Object.keys(config.agents ?? {}).sort();
29772
+ if (agentNames.length === 0) {
29773
+ return {
29774
+ name: "vault-broker per-agent socket pairs",
29775
+ status: "ok",
29776
+ detail: "no agents configured"
29777
+ };
29778
+ }
29779
+ const probe2 = opts?.probe ?? probeVaultBrokerSocketPair;
29780
+ const states = new Map;
29781
+ for (const name of agentNames) {
29782
+ states.set(name, probe2(name));
29783
+ }
29784
+ const allUnreachable = [...states.values()].every((s) => s === "unreachable");
29785
+ if (allUnreachable) {
29786
+ return {
29787
+ name: "vault-broker per-agent socket pairs",
29788
+ status: "skip",
29789
+ detail: "vault-broker container unreachable \u2014 couldn't probe per-agent socket pairs",
29790
+ fix: "Check the vault-broker service health row above; `switchroom update` brings it back"
29791
+ };
29792
+ }
29793
+ const groups = {
29794
+ "missing-unlock": [],
29795
+ "missing-data": [],
29796
+ "missing-both": [],
29797
+ unreachable: []
29798
+ };
29799
+ let okCount = 0;
29800
+ for (const [name, state] of states) {
29801
+ if (state === "ok") {
29802
+ okCount++;
29803
+ } else {
29804
+ groups[state].push(name);
29805
+ }
29806
+ }
29807
+ if (okCount === agentNames.length) {
29808
+ return {
29809
+ name: "vault-broker per-agent socket pairs",
29810
+ status: "ok",
29811
+ detail: `${okCount}/${agentNames.length} agents: data + unlock sockets bound`
29812
+ };
29813
+ }
29814
+ const parts = [];
29815
+ if (groups["missing-unlock"].length > 0) {
29816
+ parts.push(`unlock missing: ${groups["missing-unlock"].join(", ")} ` + `(the documented Telegram /vault unlock flow fails ENOENT for these)`);
29817
+ }
29818
+ if (groups["missing-data"].length > 0) {
29819
+ parts.push(`data missing: ${groups["missing-data"].join(", ")}`);
29820
+ }
29821
+ if (groups["missing-both"].length > 0) {
29822
+ parts.push(`both missing: ${groups["missing-both"].join(", ")}`);
29823
+ }
29824
+ if (groups["unreachable"].length > 0) {
29825
+ parts.push(`unreachable: ${groups["unreachable"].join(", ")}`);
29826
+ }
29827
+ return {
29828
+ name: "vault-broker per-agent socket pairs",
29829
+ status: "fail",
29830
+ detail: `${okCount}/${agentNames.length} ok \u2014 ${parts.join("; ")}`,
29831
+ fix: "Run `switchroom update` (or `switchroom apply` then recreate the " + "vault-broker container). If only the unlock half is missing, the " + "broker image predates PR #1721 \u2014 pull the latest image."
29832
+ };
29833
+ }
29745
29834
  function checkVault(config) {
29746
29835
  const vaultPath = config.vault?.path ? config.vault.path.replace(/^~/, process.env.HOME ?? "") : resolveStatePath("vault.enc");
29747
29836
  const broker = config.vault?.broker;
@@ -29760,6 +29849,7 @@ function checkVault(config) {
29760
29849
  status: "ok",
29761
29850
  detail: "Approval auth: passphrase (two-factor)"
29762
29851
  };
29852
+ const pairsResult = checkVaultBrokerSocketPairs(config);
29763
29853
  if (!existsSync50(vaultPath)) {
29764
29854
  return [
29765
29855
  postureResult,
@@ -29768,7 +29858,8 @@ function checkVault(config) {
29768
29858
  status: "warn",
29769
29859
  detail: `${vaultPath} not found`,
29770
29860
  fix: "Run `switchroom vault init` if you plan to store secrets in the vault"
29771
- }
29861
+ },
29862
+ pairsResult
29772
29863
  ];
29773
29864
  }
29774
29865
  const passphrase = process.env.SWITCHROOM_VAULT_PASSPHRASE;
@@ -29785,7 +29876,8 @@ function checkVault(config) {
29785
29876
  status: "skip",
29786
29877
  detail: "SWITCHROOM_VAULT_PASSPHRASE not set; cannot verify decrypt",
29787
29878
  fix: "Export SWITCHROOM_VAULT_PASSPHRASE to verify the vault unlocks"
29788
- }
29879
+ },
29880
+ pairsResult
29789
29881
  ];
29790
29882
  }
29791
29883
  try {
@@ -29796,7 +29888,8 @@ function checkVault(config) {
29796
29888
  name: "vault unlock",
29797
29889
  status: "ok",
29798
29890
  detail: `${keys.length} secret(s)`
29799
- }
29891
+ },
29892
+ pairsResult
29800
29893
  ];
29801
29894
  } catch (err) {
29802
29895
  return [
@@ -29806,7 +29899,8 @@ function checkVault(config) {
29806
29899
  status: "fail",
29807
29900
  detail: err.message,
29808
29901
  fix: "SWITCHROOM_VAULT_PASSPHRASE is wrong, or the vault file is corrupted"
29809
- }
29902
+ },
29903
+ pairsResult
29810
29904
  ];
29811
29905
  }
29812
29906
  }
@@ -47342,8 +47436,8 @@ var {
47342
47436
  } = import__.default;
47343
47437
 
47344
47438
  // src/build-info.ts
47345
- var VERSION = "0.13.25";
47346
- var COMMIT_SHA = "e927d05d";
47439
+ var VERSION = "0.13.27";
47440
+ var COMMIT_SHA = "a158e029";
47347
47441
 
47348
47442
  // src/cli/agent.ts
47349
47443
  init_source();
@@ -58657,20 +58751,48 @@ class VaultBroker {
58657
58751
  try {
58658
58752
  chmodSync7(abs, 432);
58659
58753
  } catch {}
58754
+ let agentUid = null;
58660
58755
  try {
58661
- const uid = allocateAgentUid(agentName);
58756
+ agentUid = allocateAgentUid(agentName);
58662
58757
  try {
58663
- chownSync2(abs, uid, uid);
58758
+ chownSync2(abs, agentUid, agentUid);
58664
58759
  } catch {}
58665
58760
  if (abs.endsWith("/sock")) {
58666
58761
  const dir = abs.slice(0, -"/sock".length);
58667
58762
  try {
58668
- chownSync2(dir, uid, uid);
58763
+ chownSync2(dir, agentUid, agentUid);
58669
58764
  } catch {}
58670
58765
  }
58671
58766
  } catch {}
58672
58767
  this.agentServers.set(abs, { server, agentName });
58673
- resolveP(agentName);
58768
+ const unlockAbs = unlockSocketFor(abs);
58769
+ if (existsSync33(unlockAbs)) {
58770
+ try {
58771
+ unlinkSync8(unlockAbs);
58772
+ } catch {}
58773
+ }
58774
+ const unlockServer = net3.createServer((sock) => {
58775
+ this._handleUnlockConnection(sock, false);
58776
+ });
58777
+ unlockServer.on("error", (err) => {
58778
+ try {
58779
+ server.close();
58780
+ } catch {}
58781
+ this.agentServers.delete(abs);
58782
+ rejectP(err);
58783
+ });
58784
+ unlockServer.listen(unlockAbs, () => {
58785
+ try {
58786
+ chmodSync7(unlockAbs, 432);
58787
+ } catch {}
58788
+ if (agentUid !== null) {
58789
+ try {
58790
+ chownSync2(unlockAbs, agentUid, agentUid);
58791
+ } catch {}
58792
+ }
58793
+ this.agentServers.set(unlockAbs, { server: unlockServer, agentName });
58794
+ resolveP(agentName);
58795
+ });
58674
58796
  });
58675
58797
  });
58676
58798
  }
@@ -16009,20 +16009,48 @@ class VaultBroker {
16009
16009
  try {
16010
16010
  chmodSync4(abs, 432);
16011
16011
  } catch {}
16012
+ let agentUid = null;
16012
16013
  try {
16013
- const uid = allocateAgentUid(agentName);
16014
+ agentUid = allocateAgentUid(agentName);
16014
16015
  try {
16015
- chownSync(abs, uid, uid);
16016
+ chownSync(abs, agentUid, agentUid);
16016
16017
  } catch {}
16017
16018
  if (abs.endsWith("/sock")) {
16018
16019
  const dir = abs.slice(0, -"/sock".length);
16019
16020
  try {
16020
- chownSync(dir, uid, uid);
16021
+ chownSync(dir, agentUid, agentUid);
16021
16022
  } catch {}
16022
16023
  }
16023
16024
  } catch {}
16024
16025
  this.agentServers.set(abs, { server, agentName });
16025
- resolveP(agentName);
16026
+ const unlockAbs = unlockSocketFor(abs);
16027
+ if (existsSync8(unlockAbs)) {
16028
+ try {
16029
+ unlinkSync4(unlockAbs);
16030
+ } catch {}
16031
+ }
16032
+ const unlockServer = net.createServer((sock) => {
16033
+ this._handleUnlockConnection(sock, false);
16034
+ });
16035
+ unlockServer.on("error", (err) => {
16036
+ try {
16037
+ server.close();
16038
+ } catch {}
16039
+ this.agentServers.delete(abs);
16040
+ rejectP(err);
16041
+ });
16042
+ unlockServer.listen(unlockAbs, () => {
16043
+ try {
16044
+ chmodSync4(unlockAbs, 432);
16045
+ } catch {}
16046
+ if (agentUid !== null) {
16047
+ try {
16048
+ chownSync(unlockAbs, agentUid, agentUid);
16049
+ } catch {}
16050
+ }
16051
+ this.agentServers.set(unlockAbs, { server: unlockServer, agentName });
16052
+ resolveP(agentName);
16053
+ });
16026
16054
  });
16027
16055
  });
16028
16056
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.25",
3
+ "version": "0.13.27",
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": {
@@ -16,7 +16,7 @@
16
16
  * any still-active reactions to 👍 before it gets SIGTERM'd.
17
17
  *
18
18
  * Both consumers call `sweepActiveReactions`, which is shaped as a
19
- * pure function that takes the setDone callback as an argument. That
19
+ * pure function that takes the finalize callback as an argument. That
20
20
  * keeps it testable in isolation — the tests pass a fake callback and
21
21
  * assert which reactions were visited and whether the sidecar was
22
22
  * cleared.
@@ -24,7 +24,7 @@
24
24
 
25
25
  import { readActiveReactions, clearActiveReactions, type ActiveReaction } from "./active-reactions.js";
26
26
 
27
- export type SetDoneReactionFn = (chatId: string, messageId: number) => Promise<unknown>;
27
+ export type FinalizeReactionFn = (chatId: string, messageId: number) => Promise<unknown>;
28
28
 
29
29
  export interface SweepOptions {
30
30
  timeoutMs?: number;
@@ -45,7 +45,7 @@ export interface SweepResult {
45
45
  */
46
46
  export async function sweepActiveReactions(
47
47
  agentDir: string,
48
- setDone: SetDoneReactionFn,
48
+ finalize: FinalizeReactionFn,
49
49
  options: SweepOptions = {},
50
50
  ): Promise<SweepResult> {
51
51
  const log = options.log ?? (() => {});
@@ -56,7 +56,7 @@ export async function sweepActiveReactions(
56
56
  log(`sweeping ${reactions.length} stale reaction(s)`);
57
57
  const attempts = reactions.map((r) =>
58
58
  Promise.resolve()
59
- .then(() => setDone(r.chatId, r.messageId))
59
+ .then(() => finalize(r.chatId, r.messageId))
60
60
  .catch((err: unknown) => {
61
61
  const msg = err instanceof Error ? err.message : String(err);
62
62
  log(`reaction sweep failed for ${r.chatId}/${r.messageId}: ${msg}`);