switchroom 0.18.24 → 0.18.25

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 (42) hide show
  1. package/dist/cli/switchroom.js +59 -11
  2. package/dist/host-control/main.js +1 -1
  3. package/package.json +1 -1
  4. package/telegram-plugin/dist/bridge/bridge.js +26 -0
  5. package/telegram-plugin/dist/gateway/gateway.js +1489 -829
  6. package/telegram-plugin/dist/server.js +26 -0
  7. package/telegram-plugin/gateway/callback-query-handlers.ts +7 -0
  8. package/telegram-plugin/gateway/gateway.ts +314 -3
  9. package/telegram-plugin/gateway/model-command.ts +188 -56
  10. package/telegram-plugin/gateway/redelivery-decision.ts +139 -0
  11. package/telegram-plugin/gateway/vault-grant-inbound-builders.ts +42 -1
  12. package/telegram-plugin/history.ts +118 -0
  13. package/telegram-plugin/registry/turns-schema.ts +89 -1
  14. package/telegram-plugin/session-tail.ts +185 -0
  15. package/telegram-plugin/subagent-watcher.ts +45 -0
  16. package/telegram-plugin/tests/crash-redelivery-resume-exclusion.test.ts +133 -0
  17. package/telegram-plugin/tests/crash-redelivery-wiring.test.ts +72 -0
  18. package/telegram-plugin/tests/history.test.ts +91 -0
  19. package/telegram-plugin/tests/model-command.test.ts +189 -12
  20. package/telegram-plugin/tests/redelivery-decision.test.ts +84 -0
  21. package/telegram-plugin/tests/registry-turns.test.ts +51 -0
  22. package/telegram-plugin/tests/session-model-source.test.ts +11 -0
  23. package/telegram-plugin/tests/session-tail.test.ts +145 -0
  24. package/telegram-plugin/tests/subagent-watcher.test.ts +50 -0
  25. package/telegram-plugin/tests/tool-activity-summary.test.ts +109 -0
  26. package/telegram-plugin/tests/trailing-answer-projector.test.ts +124 -0
  27. package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +125 -0
  28. package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +306 -0
  29. package/telegram-plugin/tool-activity-summary.ts +54 -3
  30. package/telegram-plugin/worker-activity-feed.ts +104 -0
  31. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +762 -0
  32. package/vendor/hindsight-memory/scripts/drain_pending.py +13 -1
  33. package/vendor/hindsight-memory/scripts/lib/client.py +14 -4
  34. package/vendor/hindsight-memory/scripts/lib/config.py +8 -0
  35. package/vendor/hindsight-memory/scripts/lib/pacing.py +102 -0
  36. package/vendor/hindsight-memory/scripts/lib/watermark.py +213 -0
  37. package/vendor/hindsight-memory/scripts/reconcile_tail.py +344 -0
  38. package/vendor/hindsight-memory/scripts/retain.py +299 -143
  39. package/vendor/hindsight-memory/scripts/session_start.py +14 -0
  40. package/vendor/hindsight-memory/scripts/tests/test_backfill.py +362 -0
  41. package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +350 -0
  42. package/vendor/hindsight-memory/tests/test_hooks.py +8 -2
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
2120
2120
  });
2121
2121
 
2122
2122
  // src/build-info.ts
2123
- var VERSION = "0.18.24", COMMIT_SHA = "f641363e";
2123
+ var VERSION = "0.18.25", COMMIT_SHA = "01803cff";
2124
2124
 
2125
2125
  // src/cli/resolve-version.ts
2126
2126
  import { existsSync, readFileSync } from "node:fs";
@@ -15458,18 +15458,32 @@ async function injectSlashCommand(agentName, command, opts = {}) {
15458
15458
  const socket = opts.socketName ?? defaultSocketName(agentName);
15459
15459
  const session = opts.sessionName ?? agentName;
15460
15460
  const settleMs = opts.settleMs ?? 2000;
15461
- const timeoutMs = opts.timeoutMs ?? 5000;
15461
+ const signalMode = !!(opts.successPattern || opts.errorPattern);
15462
+ const timeoutMs = opts.timeoutMs ?? (signalMode ? 8000 : 5000);
15462
15463
  return withPaneLock(`${socket}:${session}`, () => injectSlashCommandWith(makeTmuxRunner(tmuxBin), {
15463
15464
  socket,
15464
15465
  session,
15465
15466
  command: command.trim(),
15466
15467
  settleMs,
15467
15468
  timeoutMs,
15468
- precondition: opts.precondition
15469
+ precondition: opts.precondition,
15470
+ successPattern: opts.successPattern,
15471
+ errorPattern: opts.errorPattern,
15472
+ settleBeforeSendMs: opts.settleBeforeSendMs
15469
15473
  }));
15470
15474
  }
15471
15475
  async function injectSlashCommandWith(runner, args) {
15472
- const { socket, session, command, settleMs, timeoutMs, precondition } = args;
15476
+ const {
15477
+ socket,
15478
+ session,
15479
+ command,
15480
+ settleMs,
15481
+ timeoutMs,
15482
+ precondition,
15483
+ successPattern,
15484
+ errorPattern,
15485
+ settleBeforeSendMs
15486
+ } = args;
15473
15487
  let bareVerb;
15474
15488
  try {
15475
15489
  bareVerb = validateInjectCommand(command);
@@ -15510,6 +15524,17 @@ async function injectSlashCommandWith(runner, args) {
15510
15524
  errorMessage: "inject precondition returned false at write time; send aborted " + "(no keys sent)."
15511
15525
  };
15512
15526
  }
15527
+ if (settleBeforeSendMs && settleBeforeSendMs > 0) {
15528
+ const settleStart = Date.now();
15529
+ let prevSettle = runner.capture(socket, session) ?? "";
15530
+ while (Date.now() - settleStart < settleBeforeSendMs) {
15531
+ await sleep(POLL_INTERVAL_MS);
15532
+ const cur = runner.capture(socket, session) ?? "";
15533
+ if (cur === prevSettle)
15534
+ break;
15535
+ prevSettle = cur;
15536
+ }
15537
+ }
15513
15538
  const before = runner.capture(socket, session) ?? "";
15514
15539
  try {
15515
15540
  runner.send(socket, session, ["send-keys", "-l", command]);
@@ -15528,9 +15553,23 @@ async function injectSlashCommandWith(runner, args) {
15528
15553
  const start = Date.now();
15529
15554
  let last = before;
15530
15555
  let stableSince = null;
15556
+ const signalMode = !!(successPattern || errorPattern);
15531
15557
  while (Date.now() - start < timeoutMs) {
15532
15558
  await sleep(POLL_INTERVAL_MS);
15533
15559
  const cur = runner.capture(socket, session) ?? "";
15560
+ if (signalMode) {
15561
+ last = cur;
15562
+ if (cur !== before) {
15563
+ const { output: region } = diffPane(before, cur, command);
15564
+ const regionLines = region.split(`
15565
+ `).map((l) => l.trim());
15566
+ if (errorPattern && regionLines.some((l) => errorPattern.test(l)))
15567
+ break;
15568
+ if (successPattern && regionLines.some((l) => successPattern.test(l)))
15569
+ break;
15570
+ }
15571
+ continue;
15572
+ }
15534
15573
  if (cur === last && cur !== before) {
15535
15574
  if (stableSince === null) {
15536
15575
  stableSince = Date.now();
@@ -63219,8 +63258,9 @@ function isoFromTs(ts) {
63219
63258
  return null;
63220
63259
  return new Date(ts * 1000).toISOString();
63221
63260
  }
63222
- function detectTurnFindings(agent, turns) {
63261
+ function detectTurnFindings(agent, turns, opts = {}) {
63223
63262
  const findings = [];
63263
+ const silentNoopFloorTs = opts.silentNoopFloorTs ?? SILENT_NOOP_FLOOR_TS;
63224
63264
  for (const t of turns) {
63225
63265
  const tid = t.turn_id ?? "?";
63226
63266
  const st = t.status;
@@ -63254,7 +63294,7 @@ function detectTurnFindings(agent, turns) {
63254
63294
  ts
63255
63295
  });
63256
63296
  }
63257
- if (st === "complete" && tl === 0 && !synthetic) {
63297
+ if (st === "complete" && tl === 0 && !synthetic && t.ts != null && t.ts >= silentNoopFloorTs) {
63258
63298
  findings.push({
63259
63299
  signal: "silent-no-op-candidate",
63260
63300
  agent,
@@ -63302,20 +63342,20 @@ function extractTs(line) {
63302
63342
  const m = line.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?/);
63303
63343
  return m ? m[0] : null;
63304
63344
  }
63305
- function scanAgent(agent, turnsText, gatewayText) {
63345
+ function scanAgent(agent, turnsText, gatewayText, opts = {}) {
63306
63346
  const turns = parseTurns(turnsText);
63307
63347
  const status_mix = {};
63308
63348
  for (const t of turns) {
63309
63349
  const st = t.status ?? "unknown";
63310
63350
  status_mix[st] = (status_mix[st] ?? 0) + 1;
63311
63351
  }
63312
- const turnFindings = detectTurnFindings(agent, turns);
63352
+ const turnFindings = detectTurnFindings(agent, turns, opts);
63313
63353
  const { findings: gwFindings, gw_hits } = detectGatewayFindings(agent, gatewayText);
63314
63354
  const findings = [...turnFindings, ...gwFindings];
63315
63355
  const escalate = turnFindings.some((f) => f.signal === "killed-incomplete-turn" || f.signal === "hang-long-stalled" || f.signal === "silent-no-op-candidate" || f.signal === "send-failed-delivery") || gw_hits["duplicate-delivery-represent"] > 0 || gw_hits["reply-delivery-failure"] > 0;
63316
63356
  return { agent, turns: turns.length, status_mix, findings, gw_hits, escalate };
63317
63357
  }
63318
- var HANG_MS = 360000, HANG_MAXTOOLS = 2, GATEWAY_SIGNATURES;
63358
+ var HANG_MS = 360000, HANG_MAXTOOLS = 2, SILENT_NOOP_FLOOR_TS = 1783900800, GATEWAY_SIGNATURES;
63319
63359
  var init_detect = __esm(() => {
63320
63360
  GATEWAY_SIGNATURES = {
63321
63361
  "duplicate-delivery-represent": /represent duplicate-send/,
@@ -63664,7 +63704,9 @@ function runScan(opts = {}) {
63664
63704
  continue;
63665
63705
  }
63666
63706
  try {
63667
- const res = scanAgent(agent, turnsText, gwText);
63707
+ const res = scanAgent(agent, turnsText, gwText, {
63708
+ silentNoopFloorTs: opts.silentNoopFloorTs
63709
+ });
63668
63710
  perAgent.push(res);
63669
63711
  findings.push(...res.findings);
63670
63712
  scanned++;
@@ -78672,12 +78714,16 @@ var PHASE2_MIGRATIONS = [
78672
78714
  var PHASE3_MIGRATIONS = [
78673
78715
  `ALTER TABLE turns ADD COLUMN resumed_at INTEGER`
78674
78716
  ];
78717
+ var PHASE4_MIGRATIONS = [
78718
+ `ALTER TABLE turns ADD COLUMN session_id TEXT`,
78719
+ `ALTER TABLE turns ADD COLUMN answer_redelivered_at INTEGER`
78720
+ ];
78675
78721
  function applySchema(db) {
78676
78722
  db.exec("PRAGMA journal_mode = WAL");
78677
78723
  db.exec("PRAGMA synchronous = NORMAL");
78678
78724
  db.exec("PRAGMA busy_timeout = 5000");
78679
78725
  db.exec(SCHEMA_SQL);
78680
- for (const sql of [...PHASE1_MIGRATIONS, ...PHASE2_MIGRATIONS, ...PHASE3_MIGRATIONS]) {
78726
+ for (const sql of [...PHASE1_MIGRATIONS, ...PHASE2_MIGRATIONS, ...PHASE3_MIGRATIONS, ...PHASE4_MIGRATIONS]) {
78681
78727
  try {
78682
78728
  db.exec(sql);
78683
78729
  } catch (err) {
@@ -78720,6 +78766,8 @@ function mapRow(row) {
78720
78766
  tool_call_count: row.tool_call_count,
78721
78767
  interrupt_reason: row.interrupt_reason,
78722
78768
  resumed_at: row.resumed_at,
78769
+ session_id: row.session_id ?? null,
78770
+ answer_redelivered_at: row.answer_redelivered_at ?? null,
78723
78771
  created_at: row.created_at,
78724
78772
  updated_at: row.updated_at
78725
78773
  };
@@ -26605,7 +26605,7 @@ import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:f
26605
26605
  import { dirname as dirname4, join as join7 } from "node:path";
26606
26606
 
26607
26607
  // src/build-info.ts
26608
- var VERSION = "0.18.24";
26608
+ var VERSION = "0.18.25";
26609
26609
 
26610
26610
  // src/cli/resolve-version.ts
26611
26611
  function readPackageVersion() {
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.18.24",
4
+ "version": "0.18.25",
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": {
@@ -23277,6 +23277,13 @@ function projectAssistantTextBlocks(content, make) {
23277
23277
  });
23278
23278
  return out;
23279
23279
  }
23280
+ function sumUsageTokens(usage) {
23281
+ if (usage == null || typeof usage !== "object")
23282
+ return 0;
23283
+ const u = usage;
23284
+ const n = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
23285
+ return n(u.input_tokens) + n(u.output_tokens) + n(u.cache_creation_input_tokens);
23286
+ }
23280
23287
  function assistantLineCarriesAnswerSurface(content) {
23281
23288
  if (!Array.isArray(content))
23282
23289
  return false;
@@ -23332,6 +23339,15 @@ function projectTranscriptLine(line) {
23332
23339
  if (typeof mainModel === "string" && !isModelSentinel(mainModel)) {
23333
23340
  events.push({ kind: "model", model: mainModel });
23334
23341
  }
23342
+ const mainUsageTotal = sumUsageTokens(message?.usage);
23343
+ if (mainUsageTotal > 0) {
23344
+ const mainMsgId = message?.id;
23345
+ events.push({
23346
+ kind: "usage",
23347
+ messageId: typeof mainMsgId === "string" ? mainMsgId : null,
23348
+ totalTokens: mainUsageTotal
23349
+ });
23350
+ }
23335
23351
  const textEvents = projectAssistantTextBlocks(content, (text, blockIndex, lastInMessage) => ({ kind: "text", text, blockIndex, lastInMessage }));
23336
23352
  content.forEach((c, i) => {
23337
23353
  const ct = c.type;
@@ -23438,6 +23454,16 @@ function projectSubagentLine(line, agentId, state) {
23438
23454
  if (typeof subModel === "string" && !isModelSentinel(subModel)) {
23439
23455
  events.push({ kind: "sub_agent_model", agentId, model: subModel });
23440
23456
  }
23457
+ const subUsageTotal = sumUsageTokens(message?.usage);
23458
+ if (subUsageTotal > 0) {
23459
+ const subMsgId = message?.id;
23460
+ events.push({
23461
+ kind: "sub_agent_usage",
23462
+ agentId,
23463
+ messageId: typeof subMsgId === "string" ? subMsgId : null,
23464
+ totalTokens: subUsageTotal
23465
+ });
23466
+ }
23441
23467
  const textEvents = projectAssistantTextBlocks(content, (text, blockIndex, lastInMessage) => ({
23442
23468
  kind: "sub_agent_text",
23443
23469
  agentId,