switchroom 0.13.3 → 0.13.4

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.
@@ -23872,7 +23872,7 @@ import {
23872
23872
  existsSync as existsSync17,
23873
23873
  mkdirSync as mkdirSync12,
23874
23874
  readFileSync as readFileSync17,
23875
- readdirSync as readdirSync7,
23875
+ readdirSync as readdirSync8,
23876
23876
  renameSync as renameSync5,
23877
23877
  rmSync as rmSync4,
23878
23878
  statSync as statSync11,
@@ -23943,7 +23943,7 @@ function listSlots(agentDir) {
23943
23943
  if (!existsSync17(dir))
23944
23944
  return [];
23945
23945
  try {
23946
- return readdirSync7(dir).filter((name) => {
23946
+ return readdirSync8(dir).filter((name) => {
23947
23947
  try {
23948
23948
  return statSync11(join13(dir, name)).isDirectory();
23949
23949
  } catch {
@@ -24115,7 +24115,7 @@ var init_accounts = __esm(() => {
24115
24115
  import { execFileSync as execFileSync8 } from "node:child_process";
24116
24116
  import {
24117
24117
  readFileSync as readFileSync18,
24118
- readdirSync as readdirSync8,
24118
+ readdirSync as readdirSync9,
24119
24119
  existsSync as existsSync18,
24120
24120
  writeFileSync as writeFileSync10,
24121
24121
  mkdirSync as mkdirSync13,
@@ -24272,7 +24272,7 @@ function cleanupAuthTempDirs(agentDir) {
24272
24272
  if (!existsSync18(dir))
24273
24273
  return;
24274
24274
  try {
24275
- for (const entry of readdirSync8(dir)) {
24275
+ for (const entry of readdirSync9(dir)) {
24276
24276
  if (entry.startsWith(".setup-token-tmp-")) {
24277
24277
  rmSync5(join14(dir, entry), { recursive: true, force: true });
24278
24278
  }
@@ -25226,7 +25226,7 @@ class AuthBrokerClient {
25226
25226
  closed = false;
25227
25227
  constructor(opts = {}) {
25228
25228
  this.socketPath = resolveAuthBrokerSocketPath(opts);
25229
- this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
25229
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
25230
25230
  }
25231
25231
  getSocketPath() {
25232
25232
  return this.socketPath;
@@ -25477,7 +25477,7 @@ async function withAuthBrokerClient(fn, opts) {
25477
25477
  function authBrokerSocketExists(opts) {
25478
25478
  return existsSync25(resolveAuthBrokerSocketPath(opts));
25479
25479
  }
25480
- var DEFAULT_TIMEOUT_MS3 = 5000, AuthBrokerError, AuthBrokerUnreachableError;
25480
+ var DEFAULT_TIMEOUT_MS2 = 5000, AuthBrokerError, AuthBrokerUnreachableError;
25481
25481
  var init_client2 = __esm(() => {
25482
25482
  init_protocol2();
25483
25483
  AuthBrokerError = class AuthBrokerError extends Error {
@@ -25537,7 +25537,7 @@ import {
25537
25537
  existsSync as existsSync26,
25538
25538
  mkdirSync as mkdirSync15,
25539
25539
  readFileSync as readFileSync22,
25540
- readdirSync as readdirSync12,
25540
+ readdirSync as readdirSync13,
25541
25541
  renameSync as renameSync6,
25542
25542
  rmSync as rmSync10,
25543
25543
  statSync as statSync15,
@@ -25568,7 +25568,7 @@ function listAccounts(home2 = homedir8()) {
25568
25568
  if (!existsSync26(root))
25569
25569
  return [];
25570
25570
  try {
25571
- return readdirSync12(root).filter((name) => {
25571
+ return readdirSync13(root).filter((name) => {
25572
25572
  try {
25573
25573
  return statSync15(join19(root, name)).isDirectory();
25574
25574
  } catch {
@@ -25730,7 +25730,7 @@ __export(exports_oauth, {
25730
25730
  });
25731
25731
  import * as http from "node:http";
25732
25732
  import * as crypto2 from "node:crypto";
25733
- import { spawn as spawn3 } from "node:child_process";
25733
+ import { spawn as spawn2 } from "node:child_process";
25734
25734
  function detectHeadless(env2) {
25735
25735
  const hasDisplay = Boolean(env2.DISPLAY && env2.DISPLAY.trim() !== "" || env2.WAYLAND_DISPLAY && env2.WAYLAND_DISPLAY.trim() !== "");
25736
25736
  const inSsh = Boolean(env2.SSH_CONNECTION && env2.SSH_CONNECTION.trim() !== "" || env2.SSH_TTY && env2.SSH_TTY.trim() !== "");
@@ -25890,7 +25890,7 @@ function buildLoopbackAuthUrl(cfg, redirectUri, state) {
25890
25890
  u.searchParams.set("state", state);
25891
25891
  return u.toString();
25892
25892
  }
25893
- async function openBrowser(url, platform = process.platform, spawnImpl = spawn3) {
25893
+ async function openBrowser(url, platform = process.platform, spawnImpl = spawn2) {
25894
25894
  let cmd;
25895
25895
  let args;
25896
25896
  if (platform === "darwin") {
@@ -26407,7 +26407,7 @@ async function waitForApproval(opts) {
26407
26407
  const request = opts._request ?? approvalRequest;
26408
26408
  const lookup = opts._lookup ?? approvalLookup;
26409
26409
  const sleep3 = opts._sleep ?? defaultSleep;
26410
- const timeoutMs = opts.timeout_ms ?? DEFAULT_TIMEOUT_MS4;
26410
+ const timeoutMs = opts.timeout_ms ?? DEFAULT_TIMEOUT_MS3;
26411
26411
  const initialPoll = opts.initial_poll_ms ?? DEFAULT_INITIAL_POLL_MS;
26412
26412
  const maxPoll = opts.max_poll_ms ?? DEFAULT_MAX_POLL_MS;
26413
26413
  const backoff = opts.backoff ?? DEFAULT_BACKOFF;
@@ -26495,7 +26495,7 @@ async function waitForApproval(opts) {
26495
26495
  }
26496
26496
  }
26497
26497
  }
26498
- var DEFAULT_TIMEOUT_MS4 = 600000, DEFAULT_INITIAL_POLL_MS = 2000, DEFAULT_MAX_POLL_MS = 30000, DEFAULT_BACKOFF = 1.5, DENY_MODES;
26498
+ var DEFAULT_TIMEOUT_MS3 = 600000, DEFAULT_INITIAL_POLL_MS = 2000, DEFAULT_MAX_POLL_MS = 30000, DEFAULT_BACKOFF = 1.5, DENY_MODES;
26499
26499
  var init_wait = __esm(() => {
26500
26500
  init_client3();
26501
26501
  DENY_MODES = new Set(["deny", "deny_perm"]);
@@ -27053,7 +27053,7 @@ async function runViaClaude(opts) {
27053
27053
  if (tmuxHasSession(SESSION))
27054
27054
  tmuxKillSession(SESSION);
27055
27055
  }
27056
- const spawn4 = opts.spawnClaude ?? (() => {
27056
+ const spawn3 = opts.spawnClaude ?? (() => {
27057
27057
  execFileSync11("tmux", [
27058
27058
  "new-session",
27059
27059
  "-d",
@@ -27069,7 +27069,7 @@ async function runViaClaude(opts) {
27069
27069
  ], { stdio: "ignore", timeout: 5000 });
27070
27070
  });
27071
27071
  log(" Spawning claude in a tmux session to mint a broader-scope OAuth token\u2026");
27072
- spawn4();
27072
+ spawn3();
27073
27073
  try {
27074
27074
  const preFired = new Set;
27075
27075
  const phase1Deadline = Date.now() + urlTimeout;
@@ -27605,7 +27605,7 @@ var init_doctor_status = __esm(() => {
27605
27605
  import {
27606
27606
  existsSync as existsSync46,
27607
27607
  readFileSync as readFileSync42,
27608
- readdirSync as readdirSync16
27608
+ readdirSync as readdirSync17
27609
27609
  } from "node:fs";
27610
27610
  import { dirname as dirname11, join as join38 } from "node:path";
27611
27611
  import { execSync as execSync2 } from "node:child_process";
@@ -27686,7 +27686,7 @@ function probePlaywrightMcpVersion() {
27686
27686
  if (!existsSync46(npxCache))
27687
27687
  return null;
27688
27688
  try {
27689
- const entries = readdirSync16(npxCache);
27689
+ const entries = readdirSync17(npxCache);
27690
27690
  for (const entry of entries) {
27691
27691
  const pkgPath = join38(npxCache, entry, "node_modules/@playwright/mcp/package.json");
27692
27692
  if (existsSync46(pkgPath)) {
@@ -28834,7 +28834,7 @@ import { join as join42 } from "node:path";
28834
28834
  function runCredentialsMigrationChecks(config, deps = {}) {
28835
28835
  const credDir = deps.credentialsDir ?? join42(homedir23(), ".switchroom", "credentials");
28836
28836
  const existsSync49 = deps.existsSync ?? ((p) => realExistsSync2(p));
28837
- const readdirSync18 = deps.readdirSync ?? ((p) => realReaddirSync(p));
28837
+ const readdirSync19 = deps.readdirSync ?? ((p) => realReaddirSync(p));
28838
28838
  const isDirectory = deps.isDirectory ?? ((p) => {
28839
28839
  try {
28840
28840
  return realStatSync(p).isDirectory();
@@ -28847,7 +28847,7 @@ function runCredentialsMigrationChecks(config, deps = {}) {
28847
28847
  const agentNames = new Set(Object.keys(config.agents ?? {}));
28848
28848
  let entries;
28849
28849
  try {
28850
- entries = readdirSync18(credDir);
28850
+ entries = readdirSync19(credDir);
28851
28851
  } catch {
28852
28852
  return [
28853
28853
  {
@@ -29403,7 +29403,7 @@ import {
29403
29403
  lstatSync as lstatSync5,
29404
29404
  mkdirSync as mkdirSync27,
29405
29405
  readFileSync as readFileSync45,
29406
- readdirSync as readdirSync18,
29406
+ readdirSync as readdirSync19,
29407
29407
  statSync as statSync22
29408
29408
  } from "node:fs";
29409
29409
  import { dirname as dirname12, join as join45, resolve as resolve29 } from "node:path";
@@ -29413,7 +29413,7 @@ function findInNvm(bin) {
29413
29413
  if (!existsSync50(nvmRoot))
29414
29414
  return null;
29415
29415
  try {
29416
- const versions = readdirSync18(nvmRoot).sort().reverse();
29416
+ const versions = readdirSync19(nvmRoot).sort().reverse();
29417
29417
  for (const v of versions) {
29418
29418
  const candidate = join45(nvmRoot, v, "bin", bin);
29419
29419
  try {
@@ -29585,7 +29585,7 @@ function findChromium(homeDir = process.env.HOME ?? "", envBrowsersPath = proces
29585
29585
  if (!existsSync50(cacheDir))
29586
29586
  continue;
29587
29587
  try {
29588
- const entries = readdirSync18(cacheDir).filter((e) => e.startsWith("chromium"));
29588
+ const entries = readdirSync19(cacheDir).filter((e) => e.startsWith("chromium"));
29589
29589
  for (const entry of entries) {
29590
29590
  const candidates2 = [
29591
29591
  join45(cacheDir, entry, "chrome-linux64", "chrome"),
@@ -29898,7 +29898,7 @@ function checkPendingRetainsQueue(dir) {
29898
29898
  }
29899
29899
  let names;
29900
29900
  try {
29901
- names = readdirSync18(pendingDir);
29901
+ names = readdirSync19(pendingDir);
29902
29902
  } catch (err) {
29903
29903
  return {
29904
29904
  name: "pending-retains queue",
@@ -30158,7 +30158,7 @@ function checkRepoHygiene(repoRoot) {
30158
30158
  });
30159
30159
  }
30160
30160
  try {
30161
- const entries = readdirSync18(repoRoot);
30161
+ const entries = readdirSync19(repoRoot);
30162
30162
  for (const name of entries) {
30163
30163
  if (name === "clerk-export-with-secrets.tar.gz")
30164
30164
  continue;
@@ -47248,8 +47248,8 @@ var {
47248
47248
  } = import__.default;
47249
47249
 
47250
47250
  // src/build-info.ts
47251
- var VERSION = "0.13.3";
47252
- var COMMIT_SHA = "e357d33b";
47251
+ var VERSION = "0.13.4";
47252
+ var COMMIT_SHA = "b4fd0264";
47253
47253
 
47254
47254
  // src/cli/agent.ts
47255
47255
  init_source();
@@ -50468,13 +50468,11 @@ import { join as join12 } from "node:path";
50468
50468
  import { execFileSync as execFileSync6 } from "node:child_process";
50469
50469
 
50470
50470
  // src/agents/handoff-summarizer.ts
50471
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync8, renameSync as renameSync4, mkdirSync as mkdirSync11, existsSync as existsSync14, statSync as statSync9 } from "node:fs";
50471
+ import { readFileSync as readFileSync14, writeFileSync as writeFileSync8, renameSync as renameSync4, mkdirSync as mkdirSync11, existsSync as existsSync14, statSync as statSync9, readdirSync as readdirSync7 } from "node:fs";
50472
50472
  import { join as join11 } from "node:path";
50473
- import { spawn as spawn2 } from "node:child_process";
50474
- var DEFAULT_SUMMARIZER_MODEL = "claude-haiku-4-5-20251001";
50475
50473
  var DEFAULT_MAX_TURNS = 50;
50476
- var DEFAULT_TIMEOUT_MS2 = 30000;
50477
50474
  var TOPIC_MAX_CHARS = 117;
50475
+ var TURN_TEXT_MAX_CHARS = 1200;
50478
50476
  function extractTurnsFromJsonl(path, maxTurns) {
50479
50477
  let raw;
50480
50478
  try {
@@ -50507,7 +50505,8 @@ function extractTurnsFromJsonl(path, maxTurns) {
50507
50505
  }
50508
50506
  if (obj.type === "user" && obj.message && typeof obj.message === "object") {
50509
50507
  const content = obj.message.content;
50510
- const text = extractTextBlocks(content);
50508
+ const raw2 = extractTextBlocks(content);
50509
+ const text = raw2 ? extractChannelBody(raw2) : null;
50511
50510
  if (text)
50512
50511
  turns.push({ role: "user", text });
50513
50512
  continue;
@@ -50520,9 +50519,16 @@ function extractTurnsFromJsonl(path, maxTurns) {
50520
50519
  continue;
50521
50520
  }
50522
50521
  }
50523
- if (turns.length <= maxTurns)
50524
- return turns;
50525
- return turns.slice(turns.length - maxTurns);
50522
+ const deduped = [];
50523
+ for (const t of turns) {
50524
+ const prev = deduped[deduped.length - 1];
50525
+ if (prev && prev.role === t.role && prev.text === t.text)
50526
+ continue;
50527
+ deduped.push(t);
50528
+ }
50529
+ if (deduped.length <= maxTurns)
50530
+ return deduped;
50531
+ return deduped.slice(deduped.length - maxTurns);
50526
50532
  }
50527
50533
  function extractChannelBody(raw) {
50528
50534
  const m = raw.match(/<channel[^>]*>([\s\S]*?)<\/channel>/);
@@ -50553,56 +50559,48 @@ function extractTextBlocks(content) {
50553
50559
  `).trim();
50554
50560
  return joined.length > 0 ? joined : null;
50555
50561
  }
50556
- function buildHandoffPrompt(turns) {
50557
- const system = "You produce concise handoff briefings for an AI assistant that is " + "about to start a fresh session. The next session has no memory of " + `what just happened; your briefing is its only carry-over context.
50558
-
50559
- ` + `Output format \u2014 EXACTLY this structure, no preamble:
50560
- ` + `## Topic: <one short line, max 100 chars, describing what the user and assistant were most recently focused on>
50561
-
50562
- ` + `## Summary
50563
- <one paragraph, what we were working on>
50564
-
50565
- ` + `## Open threads
50566
- - <bulleted list of pending/unresolved items; empty list ok>
50567
-
50568
- ` + `## Last exchange
50569
- **User:** <verbatim or near-verbatim last user message, truncated to ~500 chars>
50570
- **Assistant:** <last assistant response, truncated to ~500 chars>
50562
+ function formatTranscriptTail(turns) {
50563
+ const header = `# Handoff \u2014 previous session
50571
50564
 
50572
- ` + `## Key decisions & facts
50573
- - <bullets; empty list ok>
50574
-
50575
- ` + `## Active files / paths
50576
- - <bullets; empty list ok>
50577
-
50578
- ` + "Keep the whole briefing under ~1500 tokens. Prefer brevity. Omit sections only if truly empty (still emit the heading with '- (none)').";
50579
- const transcript = turns.map((t) => `### ${t.role.toUpperCase()}
50580
- ${t.text}`).join(`
50565
+ ` + "You are resuming this agent's work. There is no generated summary " + "\u2014 below is the **raw tail of the previous session's transcript** " + "(oldest first, most recent last). Read it to reorient, then carry " + "on. Anything important worth keeping long-term should already be " + `in your memory files \u2014 check those too.
50566
+ `;
50567
+ if (turns.length === 0) {
50568
+ return header + `
50569
+ _(No recent turns were recoverable from the previous session.)_
50570
+ `;
50571
+ }
50572
+ const body = turns.map((t) => {
50573
+ let text = t.text;
50574
+ if (text.length > TURN_TEXT_MAX_CHARS) {
50575
+ text = text.slice(0, TURN_TEXT_MAX_CHARS) + `
50576
+ \u2026[truncated]`;
50577
+ }
50578
+ return `### ${t.role === "user" ? "User" : "Assistant"}
50579
+ ${text}`;
50580
+ }).join(`
50581
50581
 
50582
50582
  `);
50583
- const user = `Here is the recent session transcript (oldest first). Produce the handoff briefing per the specified format.
50583
+ return `${header}
50584
+ ## Recent turns
50584
50585
 
50585
- ` + transcript;
50586
- return { system, user };
50586
+ ${body}
50587
+ `;
50587
50588
  }
50588
- function parseHandoffResponse(raw) {
50589
- const lines = raw.split(/\r?\n/);
50590
- let start = 0;
50591
- while (start < lines.length && lines[start].trim() === "")
50592
- start++;
50593
- if (start >= lines.length)
50594
- return null;
50595
- const first = lines[start].trim();
50596
- const m = first.match(/^##\s*Topic:\s*(.+)$/i);
50597
- if (!m)
50598
- return null;
50599
- let topic = m[1].trim();
50600
- if (topic.length > TOPIC_MAX_CHARS) {
50601
- topic = topic.slice(0, TOPIC_MAX_CHARS) + "\u2026";
50589
+ function deriveTopic(turns) {
50590
+ for (let i = turns.length - 1;i >= 0; i--) {
50591
+ if (turns[i].role === "user")
50592
+ return clampTopic(firstLine(turns[i].text));
50602
50593
  }
50603
- const briefing = lines.slice(start).join(`
50604
- `).trim();
50605
- return { topic, briefing };
50594
+ if (turns.length > 0)
50595
+ return clampTopic(firstLine(turns[turns.length - 1].text));
50596
+ return "previous session";
50597
+ }
50598
+ function firstLine(s) {
50599
+ const line = s.split(/\r?\n/).find((l) => l.trim().length > 0) ?? s;
50600
+ return line.trim();
50601
+ }
50602
+ function clampTopic(s) {
50603
+ return s.length > TOPIC_MAX_CHARS ? s.slice(0, TOPIC_MAX_CHARS) + "\u2026" : s;
50606
50604
  }
50607
50605
  function writeSidecarsAtomic(agentDir, briefing, topic) {
50608
50606
  mkdirSync11(agentDir, { recursive: true });
@@ -50615,103 +50613,24 @@ function writeSidecarsAtomic(agentDir, briefing, topic) {
50615
50613
  renameSync4(handoffTmp, handoffPath);
50616
50614
  renameSync4(topicTmp, topicPath);
50617
50615
  }
50618
- async function summarize(opts) {
50619
- const model = opts.model ?? DEFAULT_SUMMARIZER_MODEL;
50616
+ async function buildHandoff(opts) {
50620
50617
  const maxTurns = opts.maxTurns ?? DEFAULT_MAX_TURNS;
50621
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
50622
50618
  const turns = extractTurnsFromJsonl(opts.jsonlPath, maxTurns);
50623
50619
  if (turns.length === 0) {
50624
50620
  return "no-turns";
50625
50621
  }
50626
- const runner = opts.runner ?? defaultClaudeCliRunner;
50627
- const prompt = buildHandoffPrompt(turns);
50628
- let raw;
50629
- try {
50630
- raw = await runner.run({
50631
- model,
50632
- system: prompt.system,
50633
- user: prompt.user,
50634
- timeoutMs
50635
- });
50636
- } catch (err) {
50637
- process.stderr.write(`handoff-summarizer: claude -p call failed \u2014 ${errMsg(err)}
50638
- `);
50639
- return "cli-error";
50640
- }
50641
- raw = raw.trim();
50642
- if (!raw) {
50643
- return "empty-response";
50644
- }
50645
- const parsed = parseHandoffResponse(raw);
50646
- if (!parsed) {
50647
- process.stderr.write(`handoff-summarizer: response missing '## Topic:' header; skipping
50648
- `);
50649
- return "parse-error";
50650
- }
50622
+ const briefing = formatTranscriptTail(turns);
50623
+ const topic = deriveTopic(turns);
50651
50624
  try {
50652
- writeSidecarsAtomic(opts.agentDir, parsed.briefing, parsed.topic);
50625
+ writeSidecarsAtomic(opts.agentDir, briefing, topic);
50653
50626
  } catch (err) {
50654
- process.stderr.write(`handoff-summarizer: sidecar write failed \u2014 ${errMsg(err)}
50627
+ process.stderr.write(`handoff: sidecar write failed \u2014 ${errMsg(err)}
50655
50628
  `);
50656
50629
  return "write-error";
50657
50630
  }
50658
- await mirrorToHindsight(parsed.briefing, opts).catch(() => {});
50631
+ await mirrorToHindsight(briefing, opts).catch(() => {});
50659
50632
  return "ok";
50660
50633
  }
50661
- function buildHandoffClaudeArgs(opts) {
50662
- return [
50663
- "-p",
50664
- opts.user,
50665
- "--model",
50666
- opts.model,
50667
- "--append-system-prompt",
50668
- opts.system,
50669
- "--no-session-persistence",
50670
- "--strict-mcp-config"
50671
- ];
50672
- }
50673
- var defaultClaudeCliRunner = {
50674
- async run({ model, system, user, timeoutMs }) {
50675
- return await new Promise((resolve14, reject) => {
50676
- const args = buildHandoffClaudeArgs({ model, system, user });
50677
- const env2 = {
50678
- ...process.env,
50679
- FORCE_COLOR: "0",
50680
- NO_COLOR: "1"
50681
- };
50682
- const child = spawn2("claude", args, {
50683
- stdio: ["ignore", "pipe", "pipe"],
50684
- env: env2
50685
- });
50686
- let stdout = "";
50687
- let stderr = "";
50688
- const timer = setTimeout(() => {
50689
- child.kill("SIGTERM");
50690
- reject(new Error(`timeout after ${timeoutMs}ms`));
50691
- }, timeoutMs);
50692
- child.stdout.on("data", (d) => {
50693
- stdout += d.toString("utf-8");
50694
- });
50695
- child.stderr.on("data", (d) => {
50696
- stderr += d.toString("utf-8");
50697
- });
50698
- child.on("error", (err) => {
50699
- clearTimeout(timer);
50700
- reject(err);
50701
- });
50702
- child.on("close", (code) => {
50703
- clearTimeout(timer);
50704
- if (code === 0) {
50705
- resolve14(stdout);
50706
- } else {
50707
- const tail = stderr.trim().split(`
50708
- `).slice(-3).join(" | ");
50709
- reject(new Error(`claude -p exited ${code}: ${tail || "(no stderr)"}`));
50710
- }
50711
- });
50712
- });
50713
- }
50714
- };
50715
50634
  function errMsg(err) {
50716
50635
  if (err && typeof err === "object" && "message" in err) {
50717
50636
  return String(err.message);
@@ -50742,7 +50661,7 @@ async function mirrorToHindsight(briefing, opts) {
50742
50661
  body: JSON.stringify(body)
50743
50662
  });
50744
50663
  } catch (err) {
50745
- process.stderr.write(`handoff-summarizer: hindsight mirror failed \u2014 ${errMsg(err)}
50664
+ process.stderr.write(`handoff: hindsight mirror failed \u2014 ${errMsg(err)}
50746
50665
  `);
50747
50666
  }
50748
50667
  }
@@ -50754,7 +50673,7 @@ function findLatestSessionJsonl(claudeConfigDir) {
50754
50673
  const walk = (dir) => {
50755
50674
  let entries;
50756
50675
  try {
50757
- entries = __require("fs").readdirSync(dir);
50676
+ entries = readdirSync7(dir);
50758
50677
  } catch {
50759
50678
  return;
50760
50679
  }
@@ -51708,7 +51627,7 @@ function enforceUsername(username, agentSlug, expectedUsername, loose, warn) {
51708
51627
  }
51709
51628
 
51710
51629
  // src/setup/profile-picker.ts
51711
- import { existsSync as existsSync20, readdirSync as readdirSync9, statSync as statSync13 } from "node:fs";
51630
+ import { existsSync as existsSync20, readdirSync as readdirSync10, statSync as statSync13 } from "node:fs";
51712
51631
  import { resolve as resolve16 } from "node:path";
51713
51632
  var PROFILE_GLOSSES = {
51714
51633
  default: "minimal baseline \u2014 generic chat helper, no opinion.",
@@ -51724,7 +51643,7 @@ function defaultListProfileSkills(profileName) {
51724
51643
  return [];
51725
51644
  }
51726
51645
  try {
51727
- return readdirSync9(skillsDir).filter((entry) => {
51646
+ return readdirSync10(skillsDir).filter((entry) => {
51728
51647
  try {
51729
51648
  return statSync13(resolve16(skillsDir, entry)).isDirectory();
51730
51649
  } catch {
@@ -55390,7 +55309,7 @@ init_source();
55390
55309
  init_loader();
55391
55310
  init_vault();
55392
55311
  init_hindsight();
55393
- import { readFileSync as readFileSync26, writeFileSync as writeFileSync16, copyFileSync as copyFileSync6, existsSync as existsSync30, readdirSync as readdirSync13, statSync as statSync17 } from "node:fs";
55312
+ import { readFileSync as readFileSync26, writeFileSync as writeFileSync16, copyFileSync as copyFileSync6, existsSync as existsSync30, readdirSync as readdirSync14, statSync as statSync17 } from "node:fs";
55394
55313
  import { execFileSync as execFileSync12 } from "node:child_process";
55395
55314
  import { join as join23 } from "node:path";
55396
55315
  import { homedir as homedir10 } from "node:os";
@@ -55415,7 +55334,7 @@ function findClaudeTranscripts() {
55415
55334
  const walk = (dir) => {
55416
55335
  let entries;
55417
55336
  try {
55418
- entries = readdirSync13(dir);
55337
+ entries = readdirSync14(dir);
55419
55338
  } catch {
55420
55339
  return;
55421
55340
  }
@@ -55774,13 +55693,13 @@ init_loader();
55774
55693
  init_loader();
55775
55694
  init_client();
55776
55695
  import { readFileSync as readFileSync31, existsSync as existsSync34, unlinkSync as unlinkSync9 } from "node:fs";
55777
- import { spawn as spawn4 } from "node:child_process";
55696
+ import { spawn as spawn3 } from "node:child_process";
55778
55697
 
55779
55698
  // src/vault/broker/server.ts
55780
55699
  init_compose();
55781
55700
  init_vault();
55782
55701
  import * as net3 from "node:net";
55783
- import { mkdirSync as mkdirSync20, chmodSync as chmodSync7, chownSync, existsSync as existsSync33, readFileSync as readFileSync29, readdirSync as readdirSync14, statSync as statSync19, unlinkSync as unlinkSync8, writeFileSync as writeFileSync18, renameSync as renameSync9 } from "node:fs";
55702
+ import { mkdirSync as mkdirSync20, chmodSync as chmodSync7, chownSync, existsSync as existsSync33, readFileSync as readFileSync29, readdirSync as readdirSync15, statSync as statSync19, unlinkSync as unlinkSync8, writeFileSync as writeFileSync18, renameSync as renameSync9 } from "node:fs";
55784
55703
  import { dirname as dirname6, resolve as resolve24, basename as basename5 } from "node:path";
55785
55704
  import * as os4 from "node:os";
55786
55705
  import * as path3 from "node:path";
@@ -59844,7 +59763,7 @@ async function main() {
59844
59763
  let perAgentTargets = [];
59845
59764
  try {
59846
59765
  if (existsSync33(perAgentDir)) {
59847
- const entries = readdirSync14(perAgentDir, { withFileTypes: true });
59766
+ const entries = readdirSync15(perAgentDir, { withFileTypes: true });
59848
59767
  const flat = [];
59849
59768
  const subdirs = [];
59850
59769
  for (const e of entries) {
@@ -60124,7 +60043,7 @@ function registerVaultBrokerCommand(vaultCmd, program3) {
60124
60043
  const args = ["vault", "broker", "start", "--foreground"];
60125
60044
  if (parentOpts.config)
60126
60045
  args.unshift("--config", parentOpts.config);
60127
- const child = spawn4(process.execPath, [self2, ...args], {
60046
+ const child = spawn3(process.execPath, [self2, ...args], {
60128
60047
  detached: true,
60129
60048
  stdio: "ignore"
60130
60049
  });
@@ -60411,11 +60330,11 @@ function levelLabel(level) {
60411
60330
  function printDiagnostic(d) {
60412
60331
  const glyph = levelGlyph(d.level);
60413
60332
  const label = levelLabel(d.level);
60414
- const firstLine = d.message.split(`
60333
+ const firstLine2 = d.message.split(`
60415
60334
  `)[0];
60416
60335
  const rest = d.message.split(`
60417
60336
  `).slice(1);
60418
- console.log(` ${glyph} [${label}] ${firstLine}`);
60337
+ console.log(` ${glyph} [${label}] ${firstLine2}`);
60419
60338
  for (const line of rest) {
60420
60339
  console.log(` ${source_default.gray(line)}`);
60421
60340
  }
@@ -60810,7 +60729,7 @@ import {
60810
60729
  fsyncSync as fsyncSync5,
60811
60730
  mkdirSync as mkdirSync21,
60812
60731
  openSync as openSync9,
60813
- readdirSync as readdirSync15,
60732
+ readdirSync as readdirSync16,
60814
60733
  readFileSync as readFileSync33,
60815
60734
  renameSync as renameSync10,
60816
60735
  statSync as statSync20,
@@ -60918,7 +60837,7 @@ function listBackupFiles(dir) {
60918
60837
  return [];
60919
60838
  let entries;
60920
60839
  try {
60921
- entries = readdirSync15(dir);
60840
+ entries = readdirSync16(dir);
60922
60841
  } catch {
60923
60842
  return [];
60924
60843
  }
@@ -60941,7 +60860,7 @@ function backupVault(opts) {
60941
60860
  throw new Error(`vault backup refused: source is not a valid vault envelope: ${validationError}`);
60942
60861
  }
60943
60862
  mkdirSync21(opts.destDir, { recursive: true, mode: 448 });
60944
- const dirEntries = readdirSync15(opts.destDir);
60863
+ const dirEntries = readdirSync16(opts.destDir);
60945
60864
  const offender = findAutoUnlockSibling(dirEntries);
60946
60865
  if (offender) {
60947
60866
  throw new Error(`vault backup refused: destination '${opts.destDir}' contains a file ` + `that looks like an auto-unlock credential ('${offender}'). The ` + `machine-bound auto-unlock blob MUST NOT be co-located with the ` + `encrypted vault \u2014 if they're together in version control, the ` + `passphrase gate is bypassed. Move/remove that file and retry.`);
@@ -62710,7 +62629,7 @@ import {
62710
62629
  } from "node:fs";
62711
62630
  import { resolve as resolve26, extname, join as join37, relative, dirname as dirname9 } from "node:path";
62712
62631
  import { homedir as homedir19 } from "node:os";
62713
- import { spawn as spawn5 } from "node:child_process";
62632
+ import { spawn as spawn4 } from "node:child_process";
62714
62633
  import { timingSafeEqual as timingSafeEqual2, randomBytes as randomBytes10 } from "node:crypto";
62715
62634
 
62716
62635
  // src/web/api.ts
@@ -68626,7 +68545,7 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
68626
68545
  existing.kill();
68627
68546
  ws._logProcess = null;
68628
68547
  }
68629
- const child = spawn5("docker", ["logs", "-f", "--tail", "20", containerName(agentName)], { stdio: ["ignore", "pipe", "pipe"] });
68548
+ const child = spawn4("docker", ["logs", "-f", "--tail", "20", containerName(agentName)], { stdio: ["ignore", "pipe", "pipe"] });
68630
68549
  child.on("error", (err) => {
68631
68550
  try {
68632
68551
  ws.send(JSON.stringify({
@@ -70233,7 +70152,7 @@ init_helpers();
70233
70152
  init_loader();
70234
70153
  import { resolve as resolve32 } from "node:path";
70235
70154
  function registerHandoffCommand(program3) {
70236
- program3.command("handoff <agent>", { hidden: true }).description("Summarize the agent's last session into a handoff briefing " + "(.handoff.md) and topic line (.handoff-topic). [internal \u2014 used by Stop hook]").option("--timeout <secs>", "API call timeout in seconds", "30").option("--max-turns <n>", "Max turns fed to the summarizer", String(DEFAULT_MAX_TURNS)).option("--model <id>", "Anthropic model for the summarizer", DEFAULT_SUMMARIZER_MODEL).action(withConfigError(async (agentName, opts) => {
70155
+ program3.command("handoff <agent>", { hidden: true }).description("Build the agent's session handoff sidecars \u2014 a transcript-tail " + "briefing (.handoff.md) and topic line (.handoff-topic). " + "[internal \u2014 used by the Stop hook]").option("--max-turns <n>", "Max turns kept in the handoff transcript tail", String(DEFAULT_MAX_TURNS)).action(withConfigError(async (agentName, opts) => {
70237
70156
  const config = getConfig(program3);
70238
70157
  const agentConfig = config.agents[agentName];
70239
70158
  if (!agentConfig) {
@@ -70256,17 +70175,13 @@ function registerHandoffCommand(program3) {
70256
70175
  `);
70257
70176
  return;
70258
70177
  }
70259
- const timeoutMs = Math.max(1, parseInt(opts.timeout, 10)) * 1000;
70260
70178
  const maxTurns = Math.max(1, parseInt(opts.maxTurns, 10));
70261
- const model = continuity?.summarizer_model ?? opts.model;
70262
70179
  const cappedMaxTurns = continuity?.max_turns_in_briefing ?? maxTurns;
70263
- const status = await summarize({
70180
+ const status = await buildHandoff({
70264
70181
  jsonlPath: jsonl,
70265
70182
  agentDir,
70266
70183
  agentName,
70267
- model,
70268
- maxTurns: cappedMaxTurns,
70269
- timeoutMs
70184
+ maxTurns: cappedMaxTurns
70270
70185
  });
70271
70186
  process.stderr.write(`handoff: ${status}
70272
70187
  `);
@@ -70279,7 +70194,7 @@ import {
70279
70194
  existsSync as existsSync53,
70280
70195
  mkdirSync as mkdirSync29,
70281
70196
  openSync as openSync11,
70282
- readdirSync as readdirSync19,
70197
+ readdirSync as readdirSync20,
70283
70198
  readFileSync as readFileSync48,
70284
70199
  renameSync as renameSync11,
70285
70200
  statSync as statSync24,
@@ -70768,7 +70683,7 @@ var TMP_PREFIX = `${ISSUES_FILE}.tmp-`;
70768
70683
  function sweepOrphanTmpFiles(stateDir) {
70769
70684
  let entries;
70770
70685
  try {
70771
- entries = readdirSync19(stateDir);
70686
+ entries = readdirSync20(stateDir);
70772
70687
  } catch {
70773
70688
  return;
70774
70689
  }
@@ -72341,7 +72256,7 @@ function registerSoulCommand(program3) {
72341
72256
  // src/cli/debug.ts
72342
72257
  init_helpers();
72343
72258
  init_loader();
72344
- import { existsSync as existsSync59, readFileSync as readFileSync52, readdirSync as readdirSync20, statSync as statSync25 } from "node:fs";
72259
+ import { existsSync as existsSync59, readFileSync as readFileSync52, readdirSync as readdirSync21, statSync as statSync25 } from "node:fs";
72345
72260
  import { resolve as resolve37, join as join53 } from "node:path";
72346
72261
  import { createHash as createHash12 } from "node:crypto";
72347
72262
  init_merge();
@@ -72360,7 +72275,7 @@ function findLatestTranscriptJsonl(claudeConfigDir) {
72360
72275
  if (!existsSync59(projectsDir))
72361
72276
  return;
72362
72277
  try {
72363
- const entries = readdirSync20(projectsDir, { withFileTypes: true });
72278
+ const entries = readdirSync21(projectsDir, { withFileTypes: true });
72364
72279
  let latest;
72365
72280
  for (const entry of entries) {
72366
72281
  if (!entry.isDirectory())
@@ -72598,7 +72513,7 @@ import {
72598
72513
  mkdirSync as mkdirSync32,
72599
72514
  writeFileSync as writeFileSync29,
72600
72515
  readFileSync as readFileSync53,
72601
- readdirSync as readdirSync21,
72516
+ readdirSync as readdirSync22,
72602
72517
  unlinkSync as unlinkSync12,
72603
72518
  existsSync as existsSync60,
72604
72519
  renameSync as renameSync12
@@ -72641,7 +72556,7 @@ function listRecords() {
72641
72556
  ensureDir2();
72642
72557
  const dir = registryDir();
72643
72558
  const records = [];
72644
- for (const entry of readdirSync21(dir)) {
72559
+ for (const entry of readdirSync22(dir)) {
72645
72560
  if (!entry.endsWith(".json"))
72646
72561
  continue;
72647
72562
  const id = entry.slice(0, -5);
@@ -72988,7 +72903,7 @@ init_scaffold_integration();
72988
72903
  import {
72989
72904
  chmodSync as chmodSync9,
72990
72905
  mkdirSync as mkdirSync34,
72991
- readdirSync as readdirSync22,
72906
+ readdirSync as readdirSync23,
72992
72907
  rmSync as rmSync15,
72993
72908
  writeFileSync as writeFileSync30
72994
72909
  } from "node:fs";
@@ -73161,7 +73076,7 @@ function resolveCredentialsDir(env2) {
73161
73076
  function writeSeedFile(dir, email, seed) {
73162
73077
  mkdirSync34(dir, { recursive: true, mode: 448 });
73163
73078
  chmodSync9(dir, 448);
73164
- for (const name of readdirSync22(dir)) {
73079
+ for (const name of readdirSync23(dir)) {
73165
73080
  rmSync15(join56(dir, name), { force: true, recursive: true });
73166
73081
  }
73167
73082
  const filename = encodeCredentialsFilename(email);
@@ -73281,9 +73196,9 @@ async function runDriveMcpLauncher(opts) {
73281
73196
  const tier = opts.tier ?? configSecrets.tier;
73282
73197
  const args = buildUvxArgs(tier);
73283
73198
  const env2 = buildChildEnv(process.env, credentialsDir, brokerCreds.accountEmail);
73284
- const { spawn: spawn6 } = await import("node:child_process");
73199
+ const { spawn: spawn5 } = await import("node:child_process");
73285
73200
  const os5 = await import("node:os");
73286
- const child = spawn6("uvx", args, {
73201
+ const child = spawn5("uvx", args, {
73287
73202
  stdio: ["pipe", "pipe", "inherit"],
73288
73203
  env: env2
73289
73204
  });
@@ -73318,7 +73233,7 @@ function registerDriveMcpLauncherCommand(program3) {
73318
73233
 
73319
73234
  // src/cli/apply.ts
73320
73235
  init_source();
73321
- import { accessSync as accessSync3, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync67, mkdirSync as mkdirSync36, readdirSync as readdirSync24, renameSync as renameSync13, writeFileSync as writeFileSync32 } from "node:fs";
73236
+ import { accessSync as accessSync3, constants as fsConstants6, copyFileSync as copyFileSync11, existsSync as existsSync67, mkdirSync as mkdirSync36, readdirSync as readdirSync25, renameSync as renameSync13, writeFileSync as writeFileSync32 } from "node:fs";
73322
73237
  import { mkdir, writeFile } from "node:fs/promises";
73323
73238
  import { spawnSync as childSpawnSync } from "node:child_process";
73324
73239
  import readline from "node:readline";
@@ -73875,7 +73790,7 @@ import {
73875
73790
  chownSync as chownSync2,
73876
73791
  existsSync as existsSync66,
73877
73792
  lstatSync as lstatSync7,
73878
- readdirSync as readdirSync23,
73793
+ readdirSync as readdirSync24,
73879
73794
  realpathSync as realpathSync6,
73880
73795
  statSync as statSync26
73881
73796
  } from "node:fs";
@@ -73931,7 +73846,7 @@ function restoreOperatorOwnership(home2, operatorUid, deps = {}) {
73931
73846
  });
73932
73847
  const readdir2 = deps.readdir ?? ((p) => {
73933
73848
  try {
73934
- return readdirSync23(p);
73849
+ return readdirSync24(p);
73935
73850
  } catch {
73936
73851
  return [];
73937
73852
  }
@@ -73977,7 +73892,7 @@ function resolveVaultBindMountDir(homeDir, ctx) {
73977
73892
  function inspectVaultBindMountDir(vaultDir) {
73978
73893
  if (!existsSync67(vaultDir))
73979
73894
  return { kind: "missing" };
73980
- const entries = readdirSync24(vaultDir);
73895
+ const entries = readdirSync25(vaultDir);
73981
73896
  const unknown = [];
73982
73897
  for (const name of entries) {
73983
73898
  if (KNOWN_VAULT_ARTIFACT_NAMES.has(name))
@@ -74547,7 +74462,7 @@ function runRedactStdin() {
74547
74462
  }
74548
74463
 
74549
74464
  // src/cli/status-ask.ts
74550
- import { readFileSync as readFileSync55, existsSync as existsSync68, readdirSync as readdirSync25 } from "node:fs";
74465
+ import { readFileSync as readFileSync55, existsSync as existsSync68, readdirSync as readdirSync26 } from "node:fs";
74551
74466
  import { join as join61 } from "node:path";
74552
74467
  import { homedir as homedir34 } from "node:os";
74553
74468
 
@@ -74891,7 +74806,7 @@ function resolveSources(explicitPath) {
74891
74806
  const sources = [];
74892
74807
  let entries;
74893
74808
  try {
74894
- entries = readdirSync25(agentsDir);
74809
+ entries = readdirSync26(agentsDir);
74895
74810
  } catch {
74896
74811
  return [];
74897
74812
  }
@@ -75179,7 +75094,7 @@ import {
75179
75094
  fsyncSync as fsyncSync6,
75180
75095
  mkdirSync as mkdirSync38,
75181
75096
  openSync as openSync13,
75182
- readdirSync as readdirSync26,
75097
+ readdirSync as readdirSync27,
75183
75098
  readFileSync as readFileSync57,
75184
75099
  renameSync as renameSync14,
75185
75100
  statSync as statSync27,
@@ -75299,7 +75214,7 @@ function listSkillsOverlayEntries(agent, opts = {}) {
75299
75214
  if (!existsSync70(paths.skillsDir))
75300
75215
  return [];
75301
75216
  const out = [];
75302
- for (const name of readdirSync26(paths.skillsDir)) {
75217
+ for (const name of readdirSync27(paths.skillsDir)) {
75303
75218
  if (!/\.ya?ml$/i.test(name))
75304
75219
  continue;
75305
75220
  const full = join63(paths.skillsDir, name);
@@ -75326,7 +75241,7 @@ function listOverlayEntries(agent, opts = {}) {
75326
75241
  if (!existsSync70(paths.scheduleDir))
75327
75242
  return [];
75328
75243
  const out = [];
75329
- for (const name of readdirSync26(paths.scheduleDir)) {
75244
+ for (const name of readdirSync27(paths.scheduleDir)) {
75330
75245
  if (!/\.ya?ml$/i.test(name))
75331
75246
  continue;
75332
75247
  const full = join63(paths.scheduleDir, name);
@@ -75478,7 +75393,7 @@ import {
75478
75393
  fsyncSync as fsyncSync7,
75479
75394
  mkdirSync as mkdirSync39,
75480
75395
  openSync as openSync14,
75481
- readdirSync as readdirSync27,
75396
+ readdirSync as readdirSync28,
75482
75397
  readFileSync as readFileSync58,
75483
75398
  renameSync as renameSync15,
75484
75399
  unlinkSync as unlinkSync15,
@@ -75534,7 +75449,7 @@ function listPendingScheduleEntries(agent, opts = {}) {
75534
75449
  if (!existsSync71(dir))
75535
75450
  return [];
75536
75451
  const out = [];
75537
- for (const name of readdirSync27(dir).sort()) {
75452
+ for (const name of readdirSync28(dir).sort()) {
75538
75453
  if (!name.endsWith(".meta.json"))
75539
75454
  continue;
75540
75455
  const stageId = name.slice(0, -".meta.json".length);
@@ -76272,7 +76187,7 @@ init_helpers();
76272
76187
  init_loader();
76273
76188
  import {
76274
76189
  existsSync as existsSync75,
76275
- readdirSync as readdirSync28,
76190
+ readdirSync as readdirSync29,
76276
76191
  readFileSync as readFileSync61,
76277
76192
  renameSync as renameSync16,
76278
76193
  statSync as statSync28,
@@ -76291,7 +76206,7 @@ function planCronUnitRenames(agentsDir, agents) {
76291
76206
  continue;
76292
76207
  let entries;
76293
76208
  try {
76294
- entries = readdirSync28(telegramDir);
76209
+ entries = readdirSync29(telegramDir);
76295
76210
  } catch {
76296
76211
  continue;
76297
76212
  }
@@ -76446,7 +76361,7 @@ function registerMigrateCommand(program3) {
76446
76361
  // src/cli/hostd.ts
76447
76362
  init_source();
76448
76363
  init_helpers();
76449
- import { existsSync as existsSync76, mkdirSync as mkdirSync40, readdirSync as readdirSync29, readFileSync as readFileSync62, writeFileSync as writeFileSync34, statSync as statSync29, copyFileSync as copyFileSync12 } from "node:fs";
76364
+ import { existsSync as existsSync76, mkdirSync as mkdirSync40, readdirSync as readdirSync30, readFileSync as readFileSync62, writeFileSync as writeFileSync34, statSync as statSync29, copyFileSync as copyFileSync12 } from "node:fs";
76450
76365
  import { homedir as homedir36 } from "node:os";
76451
76366
  import { join as join67 } from "node:path";
76452
76367
  import { spawnSync as spawnSync11 } from "node:child_process";
@@ -76647,7 +76562,7 @@ function doStatus() {
76647
76562
  if (existsSync76(dir)) {
76648
76563
  const entries = [];
76649
76564
  try {
76650
- for (const name of readdirSync29(dir)) {
76565
+ for (const name of readdirSync30(dir)) {
76651
76566
  if (name === "docker-compose.yml" || name.startsWith("docker-compose.yml."))
76652
76567
  continue;
76653
76568
  const sockPath = join67(dir, name, "sock");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.13.3",
3
+ "version": "0.13.4",
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": {
@@ -422,13 +422,13 @@ mkdir -p "$TELEGRAM_STATE_DIR" 2>/dev/null || true
422
422
 
423
423
  {{#if handoffEnabled}}
424
424
  # --- Session handoff briefing ---
425
- # On a normal shutdown the Stop hook writes .handoff.md (compact
426
- # summary of the prior session) into the agent dir. Here we merge that
427
- # briefing into --append-system-prompt so the fresh session wakes up
428
- # already knowing what was going on. If the prior session crashed
429
- # without firing the Stop hook, we fall back to a synchronous
430
- # summarization capped at 2s — if that doesn't finish, skip gracefully
431
- # and rely on Hindsight's per-turn auto-recall.
425
+ # On a normal shutdown the Stop hook writes .handoff.md (a bounded
426
+ # raw transcript tail of the prior session see RFC #1620) into the
427
+ # agent dir. Here we merge that into --append-system-prompt so the
428
+ # fresh session wakes up able to reorient. If the prior session
429
+ # crashed without firing the Stop hook, we fall back to building the
430
+ # tail synchronously, capped at 2s — if that doesn't finish, skip
431
+ # gracefully and rely on Hindsight's per-turn auto-recall.
432
432
  #
433
433
  # In 'handoff' mode (the default as of #362), when no .handoff.md is
434
434
  # available from the Stop hook, we also run handoff-briefing.sh which
@@ -461,7 +461,7 @@ fi
461
461
  export SWITCHROOM_HANDOFF_SHOW_LINE={{#if handoffShowLine}}true{{else}}false{{/if}}
462
462
  APPEND_PROMPT={{#if systemPromptAppendShellQuoted}}{{{systemPromptAppendShellQuoted}}}{{else}}""{{/if}}
463
463
  # Inject .handoff-briefing.md first (assembled from live sources), then
464
- # .handoff.md (LLM-generated session summary from Stop hook). If both
464
+ # .handoff.md (raw transcript tail from the Stop hook). If both
465
465
  # exist, separate them with a divider so the agent sees both.
466
466
  if [ -s "$HANDOFF_BRIEFING_FILE" ]; then
467
467
  _BRIEFING_CONTENT=$(cat "$HANDOFF_BRIEFING_FILE")
@@ -108,7 +108,7 @@ If no sub-agents are configured, do the work yourself.
108
108
 
109
109
  By default, every restart starts a **fresh `claude` session** — the in-flight transcript is NOT carried over (`session_continuity.resume_mode: handoff`, the default since switchroom #362). Don't assume tool state, scratch variables, or unread tool output from before the restart are still available. What does survive:
110
110
 
111
- - **Handoff briefing** — on a clean shutdown, the Stop hook writes a compact summary of the prior session to `.handoff.md`. On boot, start.sh injects it into your `--append-system-prompt` so you wake up already knowing what was going on. If the prior session crashed before the Stop hook fired, a live briefing is assembled from recent Telegram messages, Hindsight recall, and today's daily memory file (`.handoff-briefing.md`).
111
+ - **Handoff briefing** — on a clean shutdown, the Stop hook writes a bounded raw transcript tail of the prior session to `.handoff.md`. On boot, start.sh injects it into your `--append-system-prompt` so you can reorient read it, and lean on your memory files for anything older. If the prior session crashed before the Stop hook fired, a live briefing is assembled from recent Telegram messages, Hindsight recall, and today's daily memory file (`.handoff-briefing.md`).
112
112
  - **Hindsight memory** — auto-recall fires on every inbound user message and surfaces relevant memories from past sessions. Long-term facts, decisions, and mental models live here, not in the transcript.
113
113
  - **Telegram history** — the gateway's SQLite buffer remembers every inbound/outbound message. Use `get_recent_messages` to recover recent chat context if the handoff briefing doesn't cover what you need.
114
114
  - **`SWITCHROOM_PENDING_TURN`** — if your previous session was killed mid-turn (watchdog, SIGTERM, timeout), start.sh exports this env var plus the chat/thread/last-user-message context. Acknowledge the interruption and ask for direction rather than silently resuming.
@@ -47702,10 +47702,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
47702
47702
  }
47703
47703
 
47704
47704
  // ../src/build-info.ts
47705
- var VERSION = "0.13.3";
47706
- var COMMIT_SHA = "e357d33b";
47707
- var COMMIT_DATE = "2026-05-21T07:05:47Z";
47708
- var LATEST_PR = 1621;
47705
+ var VERSION = "0.13.4";
47706
+ var COMMIT_SHA = "b4fd0264";
47707
+ var COMMIT_DATE = "2026-05-21T09:34:13Z";
47708
+ var LATEST_PR = 1628;
47709
47709
  var COMMITS_AHEAD_OF_TAG = 0;
47710
47710
 
47711
47711
  // gateway/boot-version.ts
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Bridge-flap resilience scenario — regression guard for #1613 / #1616.
3
+ *
4
+ * ## The bug this guards
5
+ *
6
+ * The handoff-briefing summarizer shells out to a headless `claude -p`
7
+ * once per turn (handoff Stop hook). Before #1616 it ran without
8
+ * `--strict-mcp-config`, so it auto-discovered the agent's project
9
+ * `.mcp.json` and started every MCP server in it — including
10
+ * `switchroom-telegram`. That spun up a *second* telegram bridge
11
+ * process which registered against the same gateway socket as the
12
+ * live agent's real bridge; the two collided under the gateway's
13
+ * register-race close, producing an A↔B "bridge reconnect race" flap
14
+ * every ~2s for the ~7-9s the `claude -p` lived. The handoff hook
15
+ * fires every turn, so did the flap. A turn whose completion landed
16
+ * inside a flap burst could have its `turn_end` signal eaten — the
17
+ * agent looked wedged for that turn.
18
+ *
19
+ * The fix (#1616): the summarizer passes `--strict-mcp-config`, so
20
+ * the headless `claude -p` loads zero MCP servers and never spawns a
21
+ * competing bridge. The structural guard against a new offending
22
+ * callsite is `tests/bridge-flap-regression-guard.test.ts`; this
23
+ * scenario is the behavioural backstop.
24
+ *
25
+ * ## What this scenario asserts (root-cause-agnostic by design)
26
+ *
27
+ * The checks are symptom-based, so they catch a flap reintroduced by
28
+ * ANY future change — not only a regression of #1616:
29
+ *
30
+ * 1. Send a handful of DMs in succession — each drives a turn (and a
31
+ * handoff-hook fire). **Primary assertion:** every DM gets a reply
32
+ * within budget. Directly catches both the flap (eats turn_end)
33
+ * and the wedge (a zero-bridge gap strands the inbound).
34
+ * 2. **Forensic assertion:** inspect the agent's gateway-supervisor.log
35
+ * over the test window and assert the `bridge disconnected` density
36
+ * stays BELOW a flap threshold. One healthy persistent bridge
37
+ * produces only a trickle of disconnects; a sustained reconnect
38
+ * race produces dozens in tight ~2s bursts.
39
+ *
40
+ * ## Why the log inspection
41
+ *
42
+ * A flap is a server-side phenomenon that does not always surface as
43
+ * a missed reply (a burst can self-heal in ~20-30s). The `bridge
44
+ * disconnected` count is the transport-agnostic flap symptom. This
45
+ * scenario shells into the agent container via `docker exec` to read
46
+ * the gateway log; if docker is unavailable the log assertion is
47
+ * skipped with a warning and the responsiveness checks still run.
48
+ *
49
+ * ## Tolerances
50
+ *
51
+ * - `DISCONNECT_FLAP_THRESHOLD` is the max acceptable `bridge
52
+ * disconnected` count over the window. Post-#1616 a healthy ~4-turn
53
+ * run sits well under 16 (measured ~8-13, including anonymous
54
+ * probe-connection churn); a sustained flap is 20-40+. 16 sits
55
+ * comfortably in the gap.
56
+ */
57
+
58
+ import { describe, expect, it } from "vitest";
59
+ import { execSync } from "node:child_process";
60
+ import { spinUp } from "../harness.js";
61
+ import type { ObservedMessage } from "../driver.js";
62
+
63
+ const AGENT = "test-harness";
64
+ const CONTAINER = `switchroom-${AGENT}`;
65
+ const GATEWAY_LOG = "/var/log/switchroom/gateway-supervisor.log";
66
+
67
+ const DM_COUNT = 4;
68
+ const PER_DM_TIMEOUT_MS = 30_000;
69
+ const OVERALL_DEADLINE_MS = 180_000;
70
+
71
+ // Post-#1616 a healthy ~4-turn run logs ~8-13 `bridge disconnected`
72
+ // lines (one persistent bridge + anonymous probe-connection churn).
73
+ // A sustained A↔B flap produces 20-40+ in tight ~2s bursts. 16 sits
74
+ // in the gap.
75
+ const DISCONNECT_FLAP_THRESHOLD = 16;
76
+
77
+ /** Total line count of the agent's gateway-supervisor.log, or null if
78
+ * the container/log is unreachable (CI without the container). */
79
+ function gatewayLogLineCount(): number | null {
80
+ try {
81
+ const out = execSync(
82
+ `docker exec ${CONTAINER} sh -lc 'wc -l < ${GATEWAY_LOG}'`,
83
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
84
+ );
85
+ return parseInt(out.trim(), 10);
86
+ } catch {
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /** Count `bridge disconnected` lines after `sinceLine` in the log. */
92
+ function disconnectCountSince(sinceLine: number): number | null {
93
+ try {
94
+ const out = execSync(
95
+ `docker exec ${CONTAINER} sh -lc ` +
96
+ `'awk "NR>${sinceLine}" ${GATEWAY_LOG} | grep -c "bridge disconnected" || true'`,
97
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] },
98
+ );
99
+ return parseInt(out.trim(), 10);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ describe("uat: bridge-flap resilience — agent stays responsive, gateway does not flap", () => {
106
+ it(
107
+ "every DM gets a reply and the gateway does not flap across turns",
108
+ async () => {
109
+ const baselineLine = gatewayLogLineCount();
110
+ const sc = await spinUp({ agent: AGENT });
111
+ try {
112
+ const overallDeadline = Date.now() + OVERALL_DEADLINE_MS;
113
+
114
+ for (let i = 1; i <= DM_COUNT; i++) {
115
+ await sc.sendDM(`flap-resilience probe ${i}/${DM_COUNT}: reply with OK${i}`);
116
+
117
+ const remaining = Math.min(
118
+ PER_DM_TIMEOUT_MS,
119
+ overallDeadline - Date.now(),
120
+ );
121
+ expect(
122
+ remaining,
123
+ `overall deadline hit before DM ${i} — earlier turns were too slow`,
124
+ ).toBeGreaterThan(0);
125
+
126
+ const reply = await sc.expectMessage(
127
+ (m: ObservedMessage) => m.fromBot && !m.edited,
128
+ { from: "bot", timeout: remaining },
129
+ );
130
+ expect(
131
+ reply.text.length,
132
+ `DM ${i}/${DM_COUNT} produced an empty reply — a flap may have ` +
133
+ `eaten the turn_end signal`,
134
+ ).toBeGreaterThan(0);
135
+ }
136
+
137
+ // Responsiveness held for all DM_COUNT turns. Now check the
138
+ // server-side flap signal.
139
+ if (baselineLine == null) {
140
+ console.warn(
141
+ "[bridge-flap-resilience] docker exec unavailable — skipping " +
142
+ "the gateway-log flap assertion; responsiveness checks passed.",
143
+ );
144
+ return;
145
+ }
146
+
147
+ const disconnectCount = disconnectCountSince(baselineLine);
148
+ expect(
149
+ disconnectCount,
150
+ "could not read gateway log after the run — container went away",
151
+ ).not.toBeNull();
152
+ expect(
153
+ disconnectCount as number,
154
+ `gateway logged ${disconnectCount} "bridge disconnected" lines across ` +
155
+ `${DM_COUNT} turns — at/above the flap threshold ` +
156
+ `(${DISCONNECT_FLAP_THRESHOLD}). A parasitic bridge is racing the ` +
157
+ `live one — check for a headless 'claude -p' spawned without ` +
158
+ `--strict-mcp-config (#1613/#1616).`,
159
+ ).toBeLessThan(DISCONNECT_FLAP_THRESHOLD);
160
+ } finally {
161
+ await sc.tearDown();
162
+ }
163
+ },
164
+ OVERALL_DEADLINE_MS + 30_000,
165
+ );
166
+ });