switchroom 0.20.13 → 0.20.14

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.
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.20.13", COMMIT_SHA = "a73b0fd2";
2123
+ var VERSION = "0.20.14", COMMIT_SHA = "acb44b7f";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -128794,6 +128794,8 @@ function buildBankMap(result) {
128794
128794
  push(c.bank);
128795
128795
  for (const a of result.arms ?? [])
128796
128796
  push(a.bank);
128797
+ for (const p of result.phases ?? [])
128798
+ push(p.bank);
128797
128799
  const width = Math.max(2, String(ordered.length).length);
128798
128800
  const map2 = new Map;
128799
128801
  ordered.forEach((bank, i2) => map2.set(bank, pseudonym(i2 + 1, width)));
@@ -128812,7 +128814,8 @@ function anonymiseResult(result) {
128812
128814
  bankRows: (result.db?.bankRows ?? []).map((r) => ({ ...r, bank: sub(r.bank) }))
128813
128815
  },
128814
128816
  cells: (result.cells ?? []).map((c) => ({ ...c, bank: sub(c.bank) })),
128815
- arms: result.arms === null ? null : result.arms.map((a) => ({ ...a, bank: sub(a.bank) }))
128817
+ arms: (result.arms ?? null)?.map((a) => ({ ...a, bank: sub(a.bank) })) ?? null,
128818
+ phases: (result.phases ?? null)?.map((p) => ({ ...p, bank: sub(p.bank) })) ?? null
128816
128819
  }
128817
128820
  };
128818
128821
  }
@@ -128952,6 +128955,18 @@ function assertReadOnlyOrWritesAllowed(allowWrites, opts = {}) {
128952
128955
  throw new SqlError("refusing to run: the harness's database session is WRITABLE " + `(transaction_read_only=${declared ?? "?"}, temp-table write ${writeLanded ? "succeeded" : "failed"}). ` + "This harness measures a read path and must not be able to mutate a bank. " + "Pass --allow-writes only if you genuinely intend a writable session " + "(contention profile `write` needs one for its own scratch table).");
128953
128956
  }
128954
128957
  }
128958
+ function readStatsEpoch(opts = {}) {
128959
+ const rows = sql(`SELECT
128960
+ (SELECT stats_reset::text FROM pg_stat_database WHERE datname = current_database()),
128961
+ (SELECT CASE WHEN heap_blks_hit + heap_blks_read = 0 THEN NULL
128962
+ ELSE heap_blks_hit::float8 / (heap_blks_hit + heap_blks_read) END
128963
+ FROM pg_statio_user_tables WHERE relname = 'memory_units');`, opts);
128964
+ const f = (rows[0] ?? "").split("|");
128965
+ return {
128966
+ statsResetAt: (f[0] ?? "") === "" ? null : f[0],
128967
+ heapHitRatio: (f[1] ?? "") === "" ? null : num3(f[1])
128968
+ };
128969
+ }
128955
128970
  function resetStats(opts = {}) {
128956
128971
  sql("SELECT pg_stat_reset();", { ...opts, writable: true });
128957
128972
  }
@@ -129546,9 +129561,35 @@ function formatSummary(r) {
129546
129561
  L.push(` ${pad5(a.bank, 16)}${pad5(a.method, 12)}${pad5(a.fact_type, 12)}${padL(String(a.n), 5)}` + `${padL(a.p50.toFixed(1), 9)}${padL(a.p95.toFixed(1), 9)}`);
129547
129562
  }
129548
129563
  }
129564
+ L.push(...formatPhases(r.phases ?? null));
129549
129565
  return L.join(`
129550
129566
  `);
129551
129567
  }
129568
+ function formatPhases(phases) {
129569
+ if (phases === null || phases.length === 0)
129570
+ return [];
129571
+ const L = [];
129572
+ L.push("");
129573
+ L.push(" phase attribution (SEPARATE traced pass \u2014 not comparable to the latency table above)");
129574
+ L.push(` ${pad5("bank", 16)}${padL("conc", 6)}${padL("n", 5)}${padL("client ms", 11)}${padL("server ms", 11)}` + `${padL("db ms", 9)}${padL("db/server", 11)}${padL("max gain", 10)}`);
129575
+ for (const c of phases) {
129576
+ L.push(` ${pad5(c.bank, 16)}${padL(String(c.concurrency), 6)}${padL(String(c.n), 5)}` + `${padL(c.clientMsMean.toFixed(0), 11)}${padL(c.serverMsMean.toFixed(0), 11)}` + `${padL(c.dbMsMean.toFixed(0), 9)}${padL(`${round(c.dbShareOfServer * 100, 1)}%`, 11)}` + `${padL(`${round(c.maxDbSideGainFraction * 100, 1)}%`, 10)}`);
129577
+ }
129578
+ const worst = Math.max(...phases.map((c) => c.maxDbSideGainFraction));
129579
+ L.push("");
129580
+ L.push(` ceiling: a PERFECT database (every PostgreSQL call instantaneous) removes at most ` + `${round(worst * 100, 1)}% of end-to-end recall latency, at the most database-bound cell measured.`);
129581
+ L.push(" Any database-side proposal claiming more than that is refuted by this number.");
129582
+ const totals = new Map;
129583
+ for (const c of phases) {
129584
+ for (const p of c.phases)
129585
+ totals.set(p.name, (totals.get(p.name) ?? 0) + p.meanMs);
129586
+ }
129587
+ const ranked = [...totals.entries()].sort((a, b) => b[1] - a[1]).slice(0, 4);
129588
+ if (ranked.length > 0) {
129589
+ L.push(` largest phases by mean time across all cells: ${ranked.map(([n]) => n).join(", ")}`);
129590
+ }
129591
+ return L;
129592
+ }
129552
129593
  function formatReproducibility(rep) {
129553
129594
  const L = [];
129554
129595
  L.push(`reproducibility (AC1) \u2014 tolerance \u00b1${round(rep.tolerance * 100, 0)}% on p95 per cell`);
@@ -129615,6 +129656,71 @@ function toCsv(r) {
129615
129656
  `);
129616
129657
  }
129617
129658
 
129659
+ // src/hindsight-bench/phases.ts
129660
+ var DB_PHASES = ["parallel_retrieval"];
129661
+ var DB_DIAGNOSTIC_PHASES = ["connection_wait"];
129662
+ function isDiagnosticPhase(entry) {
129663
+ const details = entry?.details;
129664
+ return details?.diagnostic === true;
129665
+ }
129666
+ function extractPhases(trace) {
129667
+ const summary = trace?.summary;
129668
+ const totalS = Number(summary?.total_duration_seconds);
129669
+ const raw = summary?.phase_metrics;
129670
+ const phases = new Map;
129671
+ const diagnostics = new Map;
129672
+ if (Array.isArray(raw)) {
129673
+ for (const entry of raw) {
129674
+ const name = entry?.phase_name;
129675
+ const secs = Number(entry?.duration_seconds);
129676
+ if (typeof name !== "string" || name === "" || !Number.isFinite(secs))
129677
+ continue;
129678
+ const into = isDiagnosticPhase(entry) ? diagnostics : phases;
129679
+ into.set(name, (into.get(name) ?? 0) + secs * 1000);
129680
+ }
129681
+ }
129682
+ return { serverMs: Number.isFinite(totalS) ? totalS * 1000 : 0, phases, diagnostics };
129683
+ }
129684
+ function reducePhaseCell(bank, rows, concurrency, samples, errors4) {
129685
+ if (samples.length === 0)
129686
+ return null;
129687
+ const clientMsMean = mean(samples.map((s) => s.clientMs));
129688
+ const serverMsMean = mean(samples.map((s) => s.extracted.serverMs));
129689
+ const names = new Set;
129690
+ for (const s of samples)
129691
+ for (const n of s.extracted.phases.keys())
129692
+ names.add(n);
129693
+ const phases = [];
129694
+ for (const name of [...names].sort()) {
129695
+ const vals = samples.map((s) => s.extracted.phases.get(name) ?? 0);
129696
+ const st = summarize(vals, 0);
129697
+ phases.push({
129698
+ name,
129699
+ meanMs: st.mean,
129700
+ p95Ms: st.p95,
129701
+ shareOfServer: serverMsMean > 0 ? st.mean / serverMsMean : 0
129702
+ });
129703
+ }
129704
+ phases.sort((a, b) => b.meanMs - a.meanMs);
129705
+ const dbMsMean = DB_PHASES.reduce((acc, name) => acc + mean(samples.map((s) => s.extracted.phases.get(name) ?? 0)), 0) + DB_DIAGNOSTIC_PHASES.reduce((acc, name) => acc + mean(samples.map((s) => s.extracted.diagnostics.get(name) ?? 0)), 0);
129706
+ return {
129707
+ bank,
129708
+ rows,
129709
+ concurrency,
129710
+ n: samples.length,
129711
+ errors: errors4,
129712
+ clientMsMean,
129713
+ serverMsMean,
129714
+ dbMsMean,
129715
+ dbShareOfServer: serverMsMean > 0 ? dbMsMean / serverMsMean : 0,
129716
+ maxDbSideGainFraction: clientMsMean > 0 ? dbMsMean / clientMsMean : 0,
129717
+ phases
129718
+ };
129719
+ }
129720
+ function mean(xs) {
129721
+ return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
129722
+ }
129723
+
129618
129724
  // src/hindsight-bench/run.ts
129619
129725
  var MAX_ERROR_SAMPLES = 5;
129620
129726
  var realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
@@ -129724,6 +129830,43 @@ async function runArmSweep(opts) {
129724
129830
  rows.sort((a, b) => a.bank.localeCompare(b.bank) || b.p50 - a.p50);
129725
129831
  return rows;
129726
129832
  }
129833
+ async function runPhaseSweep(opts) {
129834
+ const { config: config2, db } = opts;
129835
+ const deps = opts.deps ?? {};
129836
+ const sleep5 = deps.sleep ?? realSleep;
129837
+ const log = deps.log ?? (() => {});
129838
+ const settleMs = opts.settleMs ?? 2000;
129839
+ const recall = deps.recall ?? ((bank, query) => recallOnce(bank, query, {
129840
+ apiUrl: config2.apiUrl,
129841
+ timeoutMs: config2.timeoutMs,
129842
+ budget: config2.budget,
129843
+ maxTokens: config2.maxTokens,
129844
+ trace: true
129845
+ }));
129846
+ const rowsFor = new Map(db.bankRows.map((b) => [b.bank, b.rows]));
129847
+ const out = [];
129848
+ for (const bank of config2.banks) {
129849
+ for (const concurrency of [...config2.concurrency].sort((a, b) => a - b)) {
129850
+ const count = Math.max(opts.samples, concurrency);
129851
+ const raw = await driveCell(bank, concurrency, count, recall);
129852
+ const ok = raw.filter((s) => s.ok);
129853
+ const samples = ok.map((s) => ({
129854
+ clientMs: s.ms,
129855
+ extracted: extractPhases(s.trace)
129856
+ }));
129857
+ const cell = reducePhaseCell(bank, rowsFor.get(bank) ?? 0, concurrency, samples, raw.length - ok.length);
129858
+ if (cell === null) {
129859
+ log(` phases: ${bank} c=${concurrency}: no successful traced calls`);
129860
+ } else {
129861
+ out.push(cell);
129862
+ log(` phases: ${bank} c=${concurrency}: n=${cell.n} db=${(cell.dbShareOfServer * 100).toFixed(1)}% of server, ` + `max end-to-end gain from a perfect DB = ${(cell.maxDbSideGainFraction * 100).toFixed(1)}%`);
129863
+ }
129864
+ if (settleMs > 0)
129865
+ await sleep5(settleMs);
129866
+ }
129867
+ }
129868
+ return out;
129869
+ }
129727
129870
 
129728
129871
  // src/hindsight-bench/types.ts
129729
129872
  var BENCH_SCHEMA_VERSION = 2;
@@ -129777,7 +129920,7 @@ function intOpt(raw, name) {
129777
129920
  return n;
129778
129921
  }
129779
129922
  function registerHindsightBenchCommand(program3) {
129780
- program3.command("hindsight-bench").description("Measure Hindsight recall latency as a function of bank size and concurrency. " + "Percentiles, not means. Read-only by default; --contention degrades the live box.").option("--api-url <url>", "Hindsight REST base", DEFAULT_API_URL).option("--container <name>", "hindsight container for the psql probes", DEFAULT_CONTAINER2).option("--banks <spec>", "all | top:<n> | spread:<n> | comma list", "spread:5").option("--concurrency <list>", "comma-separated concurrency levels", "1,4,8,16").option("--samples <n>", "recorded recalls per cell", "60").option("--warmup <n>", "discarded recalls per cell before recording", "8").option("--timeout-ms <ms>", "per-recall timeout", "30000").option("--settle-ms <ms>", "quiet period between cells", "2000").option("--budget <budget>", "recall budget", "mid").option("--max-tokens <n>", "recall max_tokens", "4096").option("--contention [profile]", "run the sweep under synthetic load: read (cache churn, SELECT-only) or " + "write (adds a WAL storm against a harness-owned scratch table; needs --allow-writes)").option("--contention-workers <n>", "concurrent load backends", String(DEFAULT_CONTENTION.workers)).option("--contention-scan-pct <pct>", "TABLESAMPLE percentage per churn scan", String(DEFAULT_CONTENTION.scanPct)).option("--contention-max-seconds <s>", "absolute in-SQL deadline for every load backend (orphan guard)", String(DEFAULT_CONTENTION.maxSeconds)).option("--reset-stats", "call pg_stat_reset() before the sweep (never implicit)", false).option("--allow-writes", "authorise a writable database session (AC5 gate)", false).option("--arms [n]", "additionally run a traced per-arm attribution pass (n samples/bank, default 5)").option("--label <text>", "free-form label recorded in the result file", "").option("--out <path>", "write the JSON result file (or the SVG in --plot mode)").option("--csv <path>", "also write a flat per-cell CSV").option("--plot <files...>", "render the chart from result files instead of measuring").option("--compare <files...>", "AC1 reproducibility verdict over two result files").option("--contention-compare <files...>", "AC4 contention verdict over two result files").option("--tolerance <fraction>", "AC1 tolerance", String(DEFAULT_TOLERANCE)).option("--json", "emit machine-readable JSON for the verdict modes", false).action(async (opts) => {
129923
+ program3.command("hindsight-bench").description("Measure Hindsight recall latency as a function of bank size and concurrency. " + "Percentiles, not means. Read-only by default; --contention degrades the live box.").option("--api-url <url>", "Hindsight REST base", DEFAULT_API_URL).option("--container <name>", "hindsight container for the psql probes", DEFAULT_CONTAINER2).option("--banks <spec>", "all | top:<n> | spread:<n> | comma list", "spread:5").option("--concurrency <list>", "comma-separated concurrency levels", "1,4,8,16").option("--samples <n>", "recorded recalls per cell", "60").option("--warmup <n>", "discarded recalls per cell before recording", "8").option("--timeout-ms <ms>", "per-recall timeout", "30000").option("--settle-ms <ms>", "quiet period between cells", "2000").option("--budget <budget>", "recall budget", "mid").option("--max-tokens <n>", "recall max_tokens", "4096").option("--contention [profile]", "run the sweep under synthetic load: read (cache churn, SELECT-only) or " + "write (adds a WAL storm against a harness-owned scratch table; needs --allow-writes)").option("--contention-workers <n>", "concurrent load backends", String(DEFAULT_CONTENTION.workers)).option("--contention-scan-pct <pct>", "TABLESAMPLE percentage per churn scan", String(DEFAULT_CONTENTION.scanPct)).option("--contention-max-seconds <s>", "absolute in-SQL deadline for every load backend (orphan guard)", String(DEFAULT_CONTENTION.maxSeconds)).option("--reset-stats", "call pg_stat_reset() before the sweep (never implicit)", false).option("--allow-writes", "authorise a writable database session (AC5 gate)", false).option("--arms [n]", "additionally run a traced per-arm attribution pass (n samples/bank, default 5)").option("--phases [n]", "additionally run a traced phase-attribution pass over the full concurrency ladder " + "(n samples/cell, default 8); reports how much end-to-end latency a perfect database could remove").option("--label <text>", "free-form label recorded in the result file", "").option("--out <path>", "write the JSON result file (or the SVG in --plot mode)").option("--csv <path>", "also write a flat per-cell CSV").option("--plot <files...>", "render the chart from result files instead of measuring").option("--compare <files...>", "AC1 reproducibility verdict over two result files").option("--contention-compare <files...>", "AC4 contention verdict over two result files").option("--tolerance <fraction>", "AC1 tolerance", String(DEFAULT_TOLERANCE)).option("--json", "emit machine-readable JSON for the verdict modes", false).action(async (opts) => {
129781
129924
  if (opts.plot !== undefined)
129782
129925
  return runPlotMode(opts);
129783
129926
  if (opts.compare !== undefined)
@@ -129856,6 +129999,7 @@ async function runMeasureMode(opts) {
129856
129999
  resetStats(sqlOpts);
129857
130000
  process.stderr.write(`${source_default.yellow("!")} pg_stat_reset() called \u2014 cumulative statistics were discarded
129858
130001
  `);
130002
+ db = { ...db, ...readStatsEpoch(sqlOpts) };
129859
130003
  } catch (e) {
129860
130004
  fail9(`--reset-stats failed: ${e.message}`);
129861
130005
  }
@@ -129881,6 +130025,9 @@ async function runMeasureMode(opts) {
129881
130025
  const armSamples = opts.arms === undefined ? 0 : opts.arms === true || opts.arms === "" ? 5 : Number(opts.arms);
129882
130026
  if (!Number.isFinite(armSamples) || armSamples < 0)
129883
130027
  fail9(`--arms must be a non-negative integer`);
130028
+ const phaseSamples = opts.phases === undefined ? 0 : opts.phases === true || opts.phases === "" ? 8 : Number(opts.phases);
130029
+ if (!Number.isFinite(phaseSamples) || phaseSamples < 0)
130030
+ fail9(`--phases must be a non-negative integer`);
129884
130031
  process.stderr.write(`sweeping ${banks.length} bank(s) \u00d7 ${concurrency.length} concurrency level(s) = ` + `${banks.length * concurrency.length} cells, ${config2.samples} samples each ` + `(+${config2.warmup} warm-up) \u00b7 contention=${profile}
129885
130032
  `);
129886
130033
  const t0 = Date.now();
@@ -129914,6 +130061,14 @@ async function runMeasureMode(opts) {
129914
130061
  });
129915
130062
  const arms = armSamples > 0 ? await runArmSweep({ config: config2, samples: armSamples, deps: { log: (m) => process.stderr.write(`${m}
129916
130063
  `) } }) : null;
130064
+ const phases = phaseSamples > 0 ? await runPhaseSweep({
130065
+ config: config2,
130066
+ db,
130067
+ samples: phaseSamples,
130068
+ settleMs: intOpt(opts.settleMs, "--settle-ms"),
130069
+ deps: { log: (m) => process.stderr.write(`${m}
130070
+ `) }
130071
+ }) : null;
129917
130072
  result = {
129918
130073
  schema: BENCH_SCHEMA_VERSION,
129919
130074
  config: config2,
@@ -129921,6 +130076,7 @@ async function runMeasureMode(opts) {
129921
130076
  instance,
129922
130077
  cells,
129923
130078
  arms,
130079
+ phases,
129924
130080
  durationS: (Date.now() - t0) / 1000
129925
130081
  };
129926
130082
  } finally {
@@ -21559,7 +21559,7 @@ function allocateAgentUid(name) {
21559
21559
  }
21560
21560
 
21561
21561
  // src/build-info.ts
21562
- var VERSION = "0.20.13";
21562
+ var VERSION = "0.20.14";
21563
21563
 
21564
21564
  // src/setup/hindsight-recall-passthrough.ts
21565
21565
  var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
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.20.13",
4
+ "version": "0.20.14",
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": {
@@ -103438,10 +103438,10 @@ function startOutboxSweep(deps) {
103438
103438
  }
103439
103439
 
103440
103440
  // ../src/build-info.ts
103441
- var VERSION2 = "0.20.13";
103442
- var COMMIT_SHA = "a73b0fd2";
103443
- var COMMIT_DATE = "2026-08-07T03:01:37Z";
103444
- var LATEST_PR = 4502;
103441
+ var VERSION2 = "0.20.14";
103442
+ var COMMIT_SHA = "acb44b7f";
103443
+ var COMMIT_DATE = "2026-08-07T07:09:13Z";
103444
+ var LATEST_PR = 4505;
103445
103445
  var COMMITS_AHEAD_OF_TAG = 0;
103446
103446
 
103447
103447
  // gateway/boot-version.ts