switchroom 0.18.6 → 0.18.7

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 (71) hide show
  1. package/dist/agent-scheduler/index.js +1 -0
  2. package/dist/auth-broker/index.js +1 -0
  3. package/dist/cli/autoaccept-poll.js +140 -33
  4. package/dist/cli/notion-write-pretool.mjs +1 -0
  5. package/dist/cli/switchroom.js +269 -56
  6. package/dist/host-control/main.js +2 -1
  7. package/dist/vault/approvals/kernel-server.js +1 -0
  8. package/dist/vault/broker/server.js +1 -0
  9. package/package.json +3 -3
  10. package/profiles/_base/cron-session.sh.hbs +55 -16
  11. package/profiles/_base/start.sh.hbs +35 -16
  12. package/profiles/default/CLAUDE.md.hbs +1 -1
  13. package/telegram-plugin/dist/bridge/bridge.js +22 -0
  14. package/telegram-plugin/dist/gateway/gateway.js +1937 -580
  15. package/telegram-plugin/dist/server.js +24 -0
  16. package/telegram-plugin/gateway/always-allow-persist-queue.ts +438 -0
  17. package/telegram-plugin/gateway/approval-timeout-inbound-builders.ts +150 -0
  18. package/telegram-plugin/gateway/clean-shutdown-marker.ts +68 -20
  19. package/telegram-plugin/gateway/gateway.ts +1071 -130
  20. package/telegram-plugin/gateway/inbound-spool.ts +2 -1
  21. package/telegram-plugin/gateway/inject-handler.test.ts +19 -0
  22. package/telegram-plugin/gateway/inject-handler.ts +17 -0
  23. package/telegram-plugin/gateway/ipc-protocol.ts +44 -2
  24. package/telegram-plugin/gateway/ipc-server.ts +40 -0
  25. package/telegram-plugin/gateway/model-command.ts +212 -51
  26. package/telegram-plugin/gateway/pending-card-expiry.ts +98 -0
  27. package/telegram-plugin/gateway/pending-card-store.ts +173 -0
  28. package/telegram-plugin/gateway/pending-inbound-buffer.ts +12 -2
  29. package/telegram-plugin/gateway/resume-inbound-builder.ts +240 -2
  30. package/telegram-plugin/gateway/session-model-source.ts +73 -0
  31. package/telegram-plugin/gateway/worker-feed-dispatch.ts +24 -1
  32. package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +30 -7
  33. package/telegram-plugin/model-label.ts +69 -0
  34. package/telegram-plugin/operator-events.ts +24 -0
  35. package/telegram-plugin/permission-diff.ts +128 -0
  36. package/telegram-plugin/registry/subagents-schema.ts +80 -1
  37. package/telegram-plugin/registry/subagents.test.ts +90 -0
  38. package/telegram-plugin/session-tail.ts +28 -0
  39. package/telegram-plugin/silent-end.ts +49 -4
  40. package/telegram-plugin/subagent-watcher.ts +222 -37
  41. package/telegram-plugin/tests/always-allow-persist-queue.test.ts +529 -0
  42. package/telegram-plugin/tests/approval-timeout-inbound-builders.test.ts +94 -0
  43. package/telegram-plugin/tests/button-tap-turn-gated.test.ts +263 -0
  44. package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +85 -27
  45. package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +4 -2
  46. package/telegram-plugin/tests/ipc-server-query-pending-permission.test.ts +157 -0
  47. package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -5
  48. package/telegram-plugin/tests/model-command.test.ts +202 -42
  49. package/telegram-plugin/tests/model-label.test.ts +64 -0
  50. package/telegram-plugin/tests/operator-events.test.ts +1 -0
  51. package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +202 -0
  52. package/telegram-plugin/tests/pending-card-expiry.test.ts +190 -0
  53. package/telegram-plugin/tests/pending-card-store.test.ts +173 -0
  54. package/telegram-plugin/tests/permission-diff.test.ts +111 -0
  55. package/telegram-plugin/tests/resume-inbound-builder.test.ts +286 -0
  56. package/telegram-plugin/tests/session-model-source.test.ts +67 -0
  57. package/telegram-plugin/tests/session-tail.test.ts +64 -0
  58. package/telegram-plugin/tests/silent-end.test.ts +46 -1
  59. package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +39 -0
  60. package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +107 -4
  61. package/telegram-plugin/tests/subagent-watcher-handback-gaps.test.ts +42 -4
  62. package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +47 -0
  63. package/telegram-plugin/tests/subagent-watcher-terminated-ids-cap.test.ts +150 -0
  64. package/telegram-plugin/tests/subagent-watcher.test.ts +54 -0
  65. package/telegram-plugin/tests/tool-activity-summary.test.ts +37 -0
  66. package/telegram-plugin/tests/typing-wrap.test.ts +23 -0
  67. package/telegram-plugin/tests/worker-activity-feed.test.ts +11 -0
  68. package/telegram-plugin/tests/worker-feed-dispatch.test.ts +126 -0
  69. package/telegram-plugin/tool-activity-summary.ts +22 -2
  70. package/telegram-plugin/typing-wrap.ts +72 -25
  71. package/telegram-plugin/worker-activity-feed.ts +9 -0
@@ -13741,6 +13741,7 @@ var init_schema = __esm(() => {
13741
13741
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
13742
13742
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
13743
13743
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
13744
+ boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart \u2014 a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart \u2014 but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
13744
13745
  session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
13745
13746
  session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
13746
13747
  }).optional();
@@ -26944,6 +26945,7 @@ ${baseAppend}` : TELEGRAM_FORMATTING_FLOOR_CARD;
26944
26945
  handoffEnabled: agentConfig.session_continuity?.enabled !== false,
26945
26946
  resumeMode: agentConfig.session_continuity?.resume_mode ?? "handoff",
26946
26947
  resumeMaxBytes: agentConfig.session_continuity?.resume_max_bytes ?? 2000000,
26948
+ bootResumeMode: agentConfig.session_continuity?.boot_resume ?? "in-flight",
26947
26949
  resumeModeHasContinuePath: (() => {
26948
26950
  const mode = agentConfig.session_continuity?.resume_mode ?? "handoff";
26949
26951
  return mode === "auto" || mode === "continue";
@@ -28046,7 +28048,8 @@ ${baseAppend}` : TELEGRAM_FORMATTING_FLOOR_CARD;
28046
28048
  sessionMaxTurns: agentConfig.session?.max_turns,
28047
28049
  handoffEnabled: agentConfig.session_continuity?.enabled !== false,
28048
28050
  resumeMode: agentConfig.session_continuity?.resume_mode ?? "handoff",
28049
- resumeMaxBytes: agentConfig.session_continuity?.resume_max_bytes ?? 2000000
28051
+ resumeMaxBytes: agentConfig.session_continuity?.resume_max_bytes ?? 2000000,
28052
+ bootResumeMode: agentConfig.session_continuity?.boot_resume ?? "in-flight"
28050
28053
  };
28051
28054
  const beforeStartSh = existsSync15(startShPath) ? readFileSync13(startShPath, "utf-8") : "";
28052
28055
  const afterStartSh = renderTemplate(join10(basePath, "start.sh.hbs"), startShContext);
@@ -30047,7 +30050,25 @@ import { chownSync as chownSync2 } from "node:fs";
30047
30050
  import { mkdir, readFile, writeFile, rename, copyFile } from "node:fs/promises";
30048
30051
  import { homedir as homedir6 } from "node:os";
30049
30052
  import { basename as basename4, dirname as dirname6, join as join13 } from "node:path";
30050
- async function resolveLiteLLMConfirmedAgents(config) {
30053
+ function agentHadLiteLLMRouting(composeContent, agentName) {
30054
+ const lines = composeContent.split(`
30055
+ `);
30056
+ const header = ` agent-${agentName}:`;
30057
+ let i = lines.indexOf(header);
30058
+ if (i < 0)
30059
+ return false;
30060
+ for (i = i + 1;i < lines.length; i++) {
30061
+ const line = lines[i];
30062
+ if (/^ {2}\S/.test(line))
30063
+ break;
30064
+ if (/^\S/.test(line))
30065
+ break;
30066
+ if (/^\s+SWITCHROOM_LITELLM:\s*"1"\s*$/.test(line))
30067
+ return true;
30068
+ }
30069
+ return false;
30070
+ }
30071
+ async function resolveLiteLLMConfirmedAgents(config, previousCompose, deps) {
30051
30072
  const confirmed = new Set;
30052
30073
  const topEnabled = config.litellm?.enabled ?? false;
30053
30074
  const optedIn = [];
@@ -30062,16 +30083,52 @@ async function resolveLiteLLMConfirmedAgents(config) {
30062
30083
  }
30063
30084
  if (optedIn.length === 0)
30064
30085
  return confirmed;
30065
- try {
30066
- const { getViaBrokerStructured: getViaBrokerStructured2 } = await Promise.resolve().then(() => (init_client(), exports_client));
30067
- for (const name of optedIn) {
30068
- try {
30069
- const r = await getViaBrokerStructured2(`litellm/${name}/api-key`);
30070
- if (r.kind === "ok")
30071
- confirmed.add(name);
30072
- } catch {}
30086
+ let getKey = deps?.getKey ?? null;
30087
+ if (!getKey) {
30088
+ try {
30089
+ ({ getViaBrokerStructured: getKey } = await Promise.resolve().then(() => (init_client(), exports_client)));
30090
+ } catch {
30091
+ getKey = null;
30073
30092
  }
30074
- } catch {}
30093
+ }
30094
+ const unreachableAgents = [];
30095
+ for (const name of optedIn) {
30096
+ if (!getKey) {
30097
+ unreachableAgents.push(name);
30098
+ continue;
30099
+ }
30100
+ try {
30101
+ const r = await getKey(`litellm/${name}/api-key`);
30102
+ if (r.kind === "ok") {
30103
+ confirmed.add(name);
30104
+ } else if (r.kind === "unreachable") {
30105
+ unreachableAgents.push(name);
30106
+ } else if (r.kind === "denied" && r.code !== "DENIED") {
30107
+ unreachableAgents.push(name);
30108
+ }
30109
+ } catch {
30110
+ unreachableAgents.push(name);
30111
+ }
30112
+ }
30113
+ if (unreachableAgents.length > 0) {
30114
+ const preserved = [];
30115
+ const couldNotPreserve = [];
30116
+ for (const name of unreachableAgents) {
30117
+ if (previousCompose && agentHadLiteLLMRouting(previousCompose, name)) {
30118
+ confirmed.add(name);
30119
+ preserved.push(name);
30120
+ } else {
30121
+ couldNotPreserve.push(name);
30122
+ }
30123
+ }
30124
+ console.warn(`switchroom: vault-broker unreachable or unable to answer (locked/internal) while resolving LiteLLM key status for ${unreachableAgents.length} opted-in agent(s): ${unreachableAgents.join(", ")}. Refusing to silently strip proxy routing (that would boot them on untracked direct OAuth). Re-run \`switchroom apply\` once the broker is back to reconcile.`);
30125
+ if (preserved.length > 0) {
30126
+ console.warn(`switchroom: PRESERVED existing proxy routing from the on-disk compose for: ${preserved.join(", ")}.`);
30127
+ }
30128
+ if (couldNotPreserve.length > 0) {
30129
+ console.warn(`switchroom: no prior routing verdict on disk for: ${couldNotPreserve.join(", ")} ` + `\u2014 leaving them unrouted (they were not previously routed).`);
30130
+ }
30131
+ }
30075
30132
  return confirmed;
30076
30133
  }
30077
30134
  function resolveHostSwitchroomConfigPath(rawPath) {
@@ -30109,7 +30166,13 @@ async function computeComposeContent(opts) {
30109
30166
  const imageTag = resolveImageTag(release);
30110
30167
  const operatorUid = resolveOperatorUid();
30111
30168
  const resolvedConfigPath = opts.switchroomConfigPath !== undefined ? resolveHostSwitchroomConfigPath(opts.switchroomConfigPath) : undefined;
30112
- const litellmConfirmedAgents = await resolveLiteLLMConfirmedAgents(opts.config);
30169
+ let previous = null;
30170
+ try {
30171
+ previous = await readFile(opts.composePath, "utf8");
30172
+ } catch {
30173
+ previous = null;
30174
+ }
30175
+ const litellmConfirmedAgents = await resolveLiteLLMConfirmedAgents(opts.config, previous);
30113
30176
  const content = generateCompose({
30114
30177
  config: opts.config,
30115
30178
  imageTag,
@@ -30121,12 +30184,6 @@ async function computeComposeContent(opts) {
30121
30184
  switchroomConfigPath: resolvedConfigPath,
30122
30185
  operatorUid
30123
30186
  });
30124
- let previous = null;
30125
- try {
30126
- previous = await readFile(opts.composePath, "utf8");
30127
- } catch {
30128
- previous = null;
30129
- }
30130
30187
  const previousImageTag = previous ? AGENT_IMAGE_TAG_RE.exec(previous)?.[1] ?? null : null;
30131
30188
  return { content, imageTag, previous, previousImageTag };
30132
30189
  }
@@ -48883,6 +48940,7 @@ var init_install_detect = () => {};
48883
48940
  // src/litellm/provision.ts
48884
48941
  var exports_provision = {};
48885
48942
  __export(exports_provision, {
48943
+ validateKey: () => validateKey,
48886
48944
  ensureTeam: () => ensureTeam,
48887
48945
  ensureKey: () => ensureKey,
48888
48946
  LiteLLMProvisionError: () => LiteLLMProvisionError
@@ -48904,6 +48962,12 @@ function looksLikeAlreadyExists(status, body) {
48904
48962
  return true;
48905
48963
  return false;
48906
48964
  }
48965
+ function looksLikeUnknownKey(status, body) {
48966
+ if (status !== 400 && status !== 401 && status !== 403 && status !== 404)
48967
+ return false;
48968
+ const t = body.toLowerCase();
48969
+ return t.includes("not found") || t.includes("does not exist") || t.includes("no such key") || t.includes("no key found");
48970
+ }
48907
48971
  async function ensureTeam(baseUrl, masterKey, teamName, fetchFn = fetch) {
48908
48972
  const url = `${normalizeBase(baseUrl)}/team/new`;
48909
48973
  let resp;
@@ -48923,7 +48987,7 @@ async function ensureTeam(baseUrl, masterKey, teamName, fetchFn = fetch) {
48923
48987
  return;
48924
48988
  throw new LiteLLMProvisionError(`LiteLLM /team/new returned ${resp.status}`, resp.status, body);
48925
48989
  }
48926
- async function ensureKey(opts, fetchFn = fetch) {
48990
+ async function generateKeyOnce(opts, fetchFn) {
48927
48991
  const url = `${normalizeBase(opts.baseUrl)}/key/generate`;
48928
48992
  const payload = {
48929
48993
  key_alias: opts.alias
@@ -48943,23 +49007,129 @@ async function ensureKey(opts, fetchFn = fetch) {
48943
49007
  body: JSON.stringify(payload)
48944
49008
  });
48945
49009
  } catch (err) {
48946
- throw new LiteLLMProvisionError(`LiteLLM /key/generate request failed: ${err.message}`);
49010
+ return {
49011
+ kind: "error",
49012
+ error: new LiteLLMProvisionError(`LiteLLM /key/generate request failed: ${err.message}`)
49013
+ };
48947
49014
  }
48948
49015
  if (!resp.ok) {
48949
49016
  const body = await safeText(resp);
48950
- throw new LiteLLMProvisionError(`LiteLLM /key/generate returned ${resp.status}`, resp.status, body);
49017
+ if (looksLikeAlreadyExists(resp.status, body)) {
49018
+ return { kind: "duplicate-alias", status: resp.status, body };
49019
+ }
49020
+ return {
49021
+ kind: "error",
49022
+ error: new LiteLLMProvisionError(`LiteLLM /key/generate returned ${resp.status}`, resp.status, body)
49023
+ };
48951
49024
  }
48952
49025
  let json;
48953
49026
  try {
48954
49027
  json = await resp.json();
48955
49028
  } catch (err) {
48956
- throw new LiteLLMProvisionError(`LiteLLM /key/generate returned non-JSON body: ${err.message}`, resp.status);
49029
+ return {
49030
+ kind: "error",
49031
+ error: new LiteLLMProvisionError(`LiteLLM /key/generate returned non-JSON body: ${err.message}`, resp.status)
49032
+ };
48957
49033
  }
48958
49034
  const key = json?.key;
48959
49035
  if (typeof key !== "string" || key.length === 0) {
48960
- throw new LiteLLMProvisionError(`LiteLLM /key/generate response missing a "key" field`, resp.status, JSON.stringify(json));
49036
+ return {
49037
+ kind: "error",
49038
+ error: new LiteLLMProvisionError(`LiteLLM /key/generate response missing a "key" field`, resp.status, JSON.stringify(json))
49039
+ };
48961
49040
  }
48962
- return { key };
49041
+ return { kind: "ok", key };
49042
+ }
49043
+ async function resolveTokensForAlias(baseUrl, masterKey, alias, fetchFn) {
49044
+ const url = `${normalizeBase(baseUrl)}/key/list?key_alias=${encodeURIComponent(alias)}` + `&return_full_object=true&include_team_keys=true`;
49045
+ let resp;
49046
+ try {
49047
+ resp = await fetchFn(url, { method: "GET", headers: authHeaders3(masterKey) });
49048
+ } catch {
49049
+ return [];
49050
+ }
49051
+ if (!resp.ok)
49052
+ return [];
49053
+ let json;
49054
+ try {
49055
+ json = await resp.json();
49056
+ } catch {
49057
+ return [];
49058
+ }
49059
+ const keys = json?.keys;
49060
+ if (!Array.isArray(keys))
49061
+ return [];
49062
+ const tokens = [];
49063
+ for (const k of keys) {
49064
+ if (typeof k === "string" && k.length > 0)
49065
+ tokens.push(k);
49066
+ else if (k && typeof k === "object") {
49067
+ const tok = k.token;
49068
+ const raw = k.key;
49069
+ if (typeof tok === "string" && tok.length > 0)
49070
+ tokens.push(tok);
49071
+ else if (typeof raw === "string" && raw.length > 0)
49072
+ tokens.push(raw);
49073
+ }
49074
+ }
49075
+ return tokens;
49076
+ }
49077
+ async function deleteOrphanedAlias(opts, fetchFn, log) {
49078
+ const tokens = await resolveTokensForAlias(opts.baseUrl, opts.masterKey, opts.alias, fetchFn);
49079
+ const body = { key_aliases: [opts.alias] };
49080
+ if (tokens.length > 0)
49081
+ body.keys = tokens;
49082
+ const manual = `Manual remediation: delete the key with alias '${opts.alias}' in the LiteLLM ` + `admin UI, or run ` + `\`curl -X POST "${normalizeBase(opts.baseUrl)}/key/delete" -H "Authorization: Bearer <master-key>" ` + `-H "content-type: application/json" -d '{"key_aliases":["${opts.alias}"]}'\`, ` + `then re-run \`switchroom apply\`.`;
49083
+ const url = `${normalizeBase(opts.baseUrl)}/key/delete`;
49084
+ let resp;
49085
+ try {
49086
+ resp = await fetchFn(url, {
49087
+ method: "POST",
49088
+ headers: authHeaders3(opts.masterKey),
49089
+ body: JSON.stringify(body)
49090
+ });
49091
+ } catch (err) {
49092
+ throw new LiteLLMProvisionError(`LiteLLM /key/delete request failed while recovering orphaned alias ` + `'${opts.alias}': ${err.message}. ${manual}`);
49093
+ }
49094
+ if (!resp.ok) {
49095
+ const errBody = await safeText(resp);
49096
+ throw new LiteLLMProvisionError(`LiteLLM /key/delete returned ${resp.status} while recovering orphaned ` + `alias '${opts.alias}'. ${manual}`, resp.status, errBody);
49097
+ }
49098
+ log(`litellm: deleted orphaned key(s) for alias '${opts.alias}'` + (tokens.length > 0 ? ` (${tokens.length} token(s) resolved)` : "") + ` \u2014 regenerating`);
49099
+ }
49100
+ async function ensureKey(opts, fetchFn = fetch) {
49101
+ const log = opts.log ?? (() => {});
49102
+ const first = await generateKeyOnce(opts, fetchFn);
49103
+ if (first.kind === "ok")
49104
+ return { key: first.key };
49105
+ if (first.kind === "error")
49106
+ throw first.error;
49107
+ log(`litellm: key_alias '${opts.alias}' already exists in LiteLLM but the vault ` + `has no recoverable key (orphaned by a prior failed provision) \u2014 ` + `self-healing: deleting the orphan and regenerating`);
49108
+ await deleteOrphanedAlias(opts, fetchFn, log);
49109
+ const second = await generateKeyOnce(opts, fetchFn);
49110
+ if (second.kind === "ok") {
49111
+ log(`litellm: regenerated key for alias '${opts.alias}' after orphan recovery`);
49112
+ return { key: second.key };
49113
+ }
49114
+ if (second.kind === "duplicate-alias") {
49115
+ throw new LiteLLMProvisionError(`LiteLLM /key/generate still reports alias '${opts.alias}' already exists ` + `after deleting the orphan \u2014 the delete did not take effect. Manual ` + `remediation: remove the key with alias '${opts.alias}' via the LiteLLM ` + `admin UI (or POST /key/delete {"key_aliases":["${opts.alias}"]}) then ` + `re-run \`switchroom apply\`.`, second.status, second.body);
49116
+ }
49117
+ throw second.error;
49118
+ }
49119
+ async function validateKey(opts, fetchFn = fetch) {
49120
+ const url = `${normalizeBase(opts.baseUrl)}/key/info?key=${encodeURIComponent(opts.key)}`;
49121
+ let resp;
49122
+ try {
49123
+ resp = await fetchFn(url, { method: "GET", headers: authHeaders3(opts.masterKey) });
49124
+ } catch (err) {
49125
+ return { kind: "unreachable", detail: err.message };
49126
+ }
49127
+ if (resp.ok)
49128
+ return { kind: "valid" };
49129
+ const body = await safeText(resp);
49130
+ if (looksLikeUnknownKey(resp.status, body))
49131
+ return { kind: "unknown" };
49132
+ return { kind: "unreachable", detail: `HTTP ${resp.status}: ${body.slice(0, 200)}` };
48963
49133
  }
48964
49134
  async function safeText(resp) {
48965
49135
  try {
@@ -49154,7 +49324,7 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
49154
49324
  const needsHindsight = config.litellm?.enabled === true && config.memory?.backend === "hindsight";
49155
49325
  if (optedIn.length === 0 && !needsHindsight)
49156
49326
  return;
49157
- const [{ getViaBrokerStructured: getViaBrokerStructured2, putViaBroker: putViaBroker2 }, { ensureTeam: ensureTeam2, ensureKey: ensureKey2 }, { addAgentSecret: addAgentSecret2 }] = await Promise.all([
49327
+ const [{ getViaBrokerStructured: getViaBrokerStructured2, putViaBroker: putViaBroker2 }, { ensureTeam: ensureTeam2, ensureKey: ensureKey2, validateKey: validateKey2 }, { addAgentSecret: addAgentSecret2 }] = await Promise.all([
49158
49328
  Promise.resolve().then(() => (init_client(), exports_client)),
49159
49329
  Promise.resolve().then(() => (init_provision(), exports_provision)),
49160
49330
  Promise.resolve().then(() => (init_telegram_yaml(), exports_telegram_yaml))
@@ -49176,7 +49346,21 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
49176
49346
  unprovisionedNew.push(name);
49177
49347
  }
49178
49348
  }
49179
- const hindsightPending = needsHindsight;
49349
+ let hindsightPending = false;
49350
+ if (needsHindsight) {
49351
+ const h = await getViaBrokerStructured3("litellm/hindsight/api-key");
49352
+ if (h.kind === "ok") {
49353
+ hindsightPending = false;
49354
+ writeOut(source_default.gray(` ~ litellm/hindsight: key already provisioned (skipped) \u2014 unaffected by missing passphrase.
49355
+ `));
49356
+ } else if (h.kind === "unreachable") {
49357
+ hindsightPending = false;
49358
+ ctx.writeErr(source_default.yellow(` ! litellm/hindsight: vault-broker unreachable \u2014 could not verify the ` + `service key; it keeps its previous state.
49359
+ `));
49360
+ } else {
49361
+ hindsightPending = true;
49362
+ }
49363
+ }
49180
49364
  if (alreadyProvisioned.length > 0) {
49181
49365
  writeOut(source_default.gray(` ~ litellm: ${alreadyProvisioned.length} agent(s) already provisioned (${alreadyProvisioned.join(", ")}) \u2014 unaffected by missing passphrase.
49182
49366
  `));
@@ -49227,43 +49411,62 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
49227
49411
  continue;
49228
49412
  }
49229
49413
  const vaultKey = `litellm/${name}/api-key`;
49414
+ const alias = `agent:${name}`;
49230
49415
  const existing = await getViaBrokerStructured2(vaultKey);
49231
- if (existing.kind === "ok") {
49232
- writeOut(source_default.gray(` ~ litellm/${name}: key already provisioned (skipped)
49233
- `));
49234
- if (configText !== null) {
49235
- const after = addAgentSecret2(configText, name, vaultKey);
49236
- if (after !== configText) {
49237
- configText = after;
49238
- pendingConfigEdits = true;
49239
- }
49240
- }
49241
- continue;
49242
- }
49243
49416
  if (existing.kind === "unreachable") {
49244
49417
  ctx.writeErr(source_default.yellow(` ! litellm/${name}: vault-broker unreachable (${existing.msg ?? "no detail"}) \u2014 ` + `skipping key provisioning; agent keeps its previous routing env.
49245
49418
  `));
49246
49419
  continue;
49247
49420
  }
49248
49421
  let masterKey = adminKeyRef;
49422
+ let masterKeyErr = null;
49249
49423
  if (isVaultReference(adminKeyRef)) {
49250
49424
  const refKey = parseVaultReference(adminKeyRef);
49251
49425
  const resolved = await getViaBrokerStructured2(refKey);
49252
49426
  if (resolved.kind !== "ok") {
49253
- failures.push({
49254
- agent: name,
49255
- message: `litellm: admin_key vault ref '${adminKeyRef}' did not resolve (${resolved.kind}${"msg" in resolved && resolved.msg ? `: ${resolved.msg}` : ""}).`
49256
- });
49257
- continue;
49427
+ masterKey = null;
49428
+ masterKeyErr = `litellm: admin_key vault ref '${adminKeyRef}' did not resolve (${resolved.kind}${"msg" in resolved && resolved.msg ? `: ${resolved.msg}` : ""}).`;
49429
+ } else if (resolved.entry.kind !== "string") {
49430
+ masterKey = null;
49431
+ masterKeyErr = `litellm: admin_key vault ref '${adminKeyRef}' is not a string secret.`;
49432
+ } else {
49433
+ masterKey = resolved.entry.value;
49258
49434
  }
49259
- if (resolved.entry.kind !== "string") {
49260
- failures.push({
49261
- agent: name,
49262
- message: `litellm: admin_key vault ref '${adminKeyRef}' is not a string secret.`
49263
- });
49435
+ }
49436
+ if (existing.kind === "ok") {
49437
+ const storedKey = existing.entry.kind === "string" ? existing.entry.value : null;
49438
+ let driftReprovision = false;
49439
+ if (storedKey && masterKey) {
49440
+ const validation = await validateKey2({ baseUrl, masterKey, key: storedKey });
49441
+ if (validation.kind === "unknown") {
49442
+ driftReprovision = true;
49443
+ ctx.writeErr(source_default.yellow(` ! litellm/${name}: stored virtual key not recognized by the proxy ` + `(DB drift) \u2014 re-provisioning a fresh key.
49444
+ `));
49445
+ } else if (validation.kind === "unreachable") {
49446
+ ctx.writeErr(source_default.yellow(` ! litellm/${name}: proxy unreachable during key validation (${validation.detail}) \u2014 keeping the stored key unverified.
49447
+ `));
49448
+ } else {
49449
+ writeOut(source_default.gray(` ~ litellm/${name}: key already provisioned + validated (skipped)
49450
+ `));
49451
+ }
49452
+ } else {
49453
+ writeOut(source_default.gray(` ~ litellm/${name}: key already provisioned (skipped)
49454
+ `));
49455
+ }
49456
+ if (!driftReprovision) {
49457
+ if (configText !== null) {
49458
+ const after = addAgentSecret2(configText, name, vaultKey);
49459
+ if (after !== configText) {
49460
+ configText = after;
49461
+ pendingConfigEdits = true;
49462
+ }
49463
+ }
49264
49464
  continue;
49265
49465
  }
49266
- masterKey = resolved.entry.value;
49466
+ }
49467
+ if (!masterKey) {
49468
+ failures.push({ agent: name, message: masterKeyErr ?? `litellm: admin_key unresolved` });
49469
+ continue;
49267
49470
  }
49268
49471
  await ensureTeam2(baseUrl, masterKey, team);
49269
49472
  const metadata = {
@@ -49281,9 +49484,11 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
49281
49484
  const { key } = await ensureKey2({
49282
49485
  baseUrl,
49283
49486
  masterKey,
49284
- alias: `agent:${name}`,
49487
+ alias,
49285
49488
  team,
49286
- metadata
49489
+ metadata,
49490
+ log: (m) => ctx.writeErr(source_default.gray(` ~ litellm/${name}: ${m}
49491
+ `))
49287
49492
  });
49288
49493
  const put = await putViaBroker2(vaultKey, { kind: "string", value: key }, { passphrase });
49289
49494
  if (put.kind !== "ok") {
@@ -49345,7 +49550,9 @@ async function provisionLiteLLMKeys(config, agentNames, switchroomConfigPath, ct
49345
49550
  masterKey,
49346
49551
  alias: "service:hindsight",
49347
49552
  team,
49348
- metadata: { service: "hindsight", env: "fleet", ...oauthAccount ? { oauth_account: oauthAccount } : {} }
49553
+ metadata: { service: "hindsight", env: "fleet", ...oauthAccount ? { oauth_account: oauthAccount } : {} },
49554
+ log: (m) => ctx.writeErr(source_default.gray(` ~ litellm/hindsight: ${m}
49555
+ `))
49349
49556
  });
49350
49557
  const put = await putViaBroker2(hindsightVaultKey, { kind: "string", value: key }, { passphrase });
49351
49558
  if (put.kind === "ok") {
@@ -64682,8 +64889,8 @@ import { existsSync, readFileSync } from "node:fs";
64682
64889
  import { dirname, join } from "node:path";
64683
64890
 
64684
64891
  // src/build-info.ts
64685
- var VERSION = "0.18.6";
64686
- var COMMIT_SHA = "b62d558a";
64892
+ var VERSION = "0.18.7";
64893
+ var COMMIT_SHA = "21478348";
64687
64894
 
64688
64895
  // src/cli/resolve-version.ts
64689
64896
  function readPackageVersion() {
@@ -79554,7 +79761,8 @@ var SUBAGENTS_SCHEMA_SQL = `
79554
79761
  status TEXT NOT NULL,
79555
79762
  result_summary TEXT,
79556
79763
  jsonl_agent_id TEXT,
79557
- parent_agent_id TEXT
79764
+ parent_agent_id TEXT,
79765
+ model TEXT
79558
79766
  );
79559
79767
  CREATE INDEX IF NOT EXISTS subagents_turn ON subagents(parent_turn_key);
79560
79768
  CREATE INDEX IF NOT EXISTS subagents_status ON subagents(status);
@@ -79570,6 +79778,10 @@ function applySubagentsSchema(db) {
79570
79778
  if (!hasParentAgentId) {
79571
79779
  db.exec("ALTER TABLE subagents ADD COLUMN parent_agent_id TEXT");
79572
79780
  }
79781
+ const hasModel = cols.some((c) => c.name === "model");
79782
+ if (!hasModel) {
79783
+ db.exec("ALTER TABLE subagents ADD COLUMN model TEXT");
79784
+ }
79573
79785
  db.exec("CREATE INDEX IF NOT EXISTS subagents_jsonl_id ON subagents(jsonl_agent_id)");
79574
79786
  }
79575
79787
  function mapSubagentRow(row) {
@@ -79586,7 +79798,8 @@ function mapSubagentRow(row) {
79586
79798
  status: row.status,
79587
79799
  result_summary: row.result_summary,
79588
79800
  jsonl_agent_id: row.jsonl_agent_id,
79589
- parent_agent_id: row.parent_agent_id ?? null
79801
+ parent_agent_id: row.parent_agent_id ?? null,
79802
+ model: row.model ?? null
79590
79803
  };
79591
79804
  }
79592
79805
  function listSubagents(db, opts = {}) {
@@ -21495,6 +21495,7 @@ var SessionContinuitySchema = exports_external.object({
21495
21495
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
21496
21496
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
21497
21497
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
21498
+ boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart — a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart — but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
21498
21499
  session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
21499
21500
  session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
21500
21501
  }).optional();
@@ -28737,7 +28738,7 @@ import { existsSync as existsSync7, readFileSync as readFileSync5 } from "node:f
28737
28738
  import { dirname as dirname4, join as join6 } from "node:path";
28738
28739
 
28739
28740
  // src/build-info.ts
28740
- var VERSION = "0.18.6";
28741
+ var VERSION = "0.18.7";
28741
28742
 
28742
28743
  // src/cli/resolve-version.ts
28743
28744
  function readPackageVersion() {
@@ -4212,6 +4212,7 @@ var init_schema = __esm(() => {
4212
4212
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
4213
4213
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
4214
4214
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
4215
+ boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart — a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart — but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
4215
4216
  session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
4216
4217
  session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
4217
4218
  }).optional();
@@ -4212,6 +4212,7 @@ var init_schema = __esm(() => {
4212
4212
  max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
4213
4213
  resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
4214
4214
  resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
4215
+ boot_resume: exports_external.enum(["always", "in-flight", "never"]).optional().describe("How the gateway auto-resumes a turn that was IN FLIGHT when the " + "agent restarted. 'in-flight' (default) resumes genuinely " + "interrupted work even after a deliberate/operator restart — a " + "sanctioned restart landing mid-turn no longer silently drops the " + "work. 'always' forces resume unconditionally (same as the " + "SWITCHROOM_BOOT_RESUME_ALWAYS=1 escape hatch). 'never' is the " + "quota-saving posture: don't auto-replay work across a clean " + "restart — but the user is STILL sent a passive notice of what was " + "in flight (silence is never used). Independent of the at-most-once " + "resume ledger and the bounded resume-chain loop-guard, which always " + "apply. Threaded to the gateway as SWITCHROOM_BOOT_RESUME."),
4215
4216
  session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
4216
4217
  session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
4217
4218
  }).optional();
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.6",
4
+ "version": "0.18.7",
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": {
@@ -24,9 +24,9 @@
24
24
  "build": "node scripts/build.mjs",
25
25
  "build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
26
26
  "pretest": "npm run build",
27
- "test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
27
+ "test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
28
28
  "test:vitest": "vitest run",
29
- "test:bun": "bun test src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
29
+ "test:bun": "bun test src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
30
30
  "test:watch": "vitest",
31
31
  "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
32
32
  "lint:tsc": "tsc --noEmit",