switchroom 0.19.27 → 0.19.29

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 (44) hide show
  1. package/dist/agent-scheduler/index.js +16 -2
  2. package/dist/auth-broker/index.js +141 -8
  3. package/dist/cli/autoaccept-poll.js +225 -17
  4. package/dist/cli/notion-write-pretool.mjs +18 -2
  5. package/dist/cli/switchroom.js +864 -43
  6. package/dist/host-control/main.js +144 -9
  7. package/dist/vault/approvals/kernel-server.js +146 -9
  8. package/dist/vault/broker/server.js +146 -9
  9. package/package.json +3 -2
  10. package/profiles/_base/start.sh.hbs +79 -15
  11. package/telegram-plugin/dist/bridge/bridge.js +1 -0
  12. package/telegram-plugin/dist/gateway/gateway.js +585 -50
  13. package/telegram-plugin/dist/server.js +1 -0
  14. package/telegram-plugin/edit-flood-fuse.ts +230 -27
  15. package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
  16. package/telegram-plugin/gateway/gateway.ts +9 -2
  17. package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
  18. package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
  19. package/telegram-plugin/mcp-credential-failure.ts +459 -0
  20. package/telegram-plugin/operator-events.ts +38 -0
  21. package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +58 -1
  22. package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -0
  23. package/telegram-plugin/tests/finalize-callback-flood-policy.test.ts +298 -0
  24. package/telegram-plugin/tests/finalize-callback.test.ts +41 -8
  25. package/telegram-plugin/tests/mcp-credential-failure.test.ts +310 -0
  26. package/vendor/hindsight-memory/CHANGELOG.md +50 -0
  27. package/vendor/hindsight-memory/scripts/backfill_transcripts.py +1 -0
  28. package/vendor/hindsight-memory/scripts/drain_pending.py +437 -11
  29. package/vendor/hindsight-memory/scripts/lib/client.py +14 -0
  30. package/vendor/hindsight-memory/scripts/lib/config.py +91 -0
  31. package/vendor/hindsight-memory/scripts/lib/pending.py +199 -28
  32. package/vendor/hindsight-memory/scripts/reconcile_tail.py +1 -0
  33. package/vendor/hindsight-memory/scripts/retain.py +41 -1
  34. package/vendor/hindsight-memory/scripts/subagent_retain.py +1 -0
  35. package/vendor/hindsight-memory/scripts/tests/test_backfill.py +23 -1
  36. package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +5 -1
  37. package/vendor/hindsight-memory/scripts/tests/test_drain_circuit_breaker.py +401 -0
  38. package/vendor/hindsight-memory/scripts/tests/test_drain_serialisation.py +286 -0
  39. package/vendor/hindsight-memory/scripts/tests/test_observation_scopes.py +325 -0
  40. package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
  41. package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +160 -1
  42. package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +40 -2
  43. package/vendor/hindsight-memory/settings.json +1 -1
  44. package/vendor/hindsight-memory/tests/test_hooks.py +11 -2
@@ -295,6 +295,182 @@ function queryPendingPermission(agentName, opts = {}) {
295
295
  });
296
296
  }
297
297
 
298
+ // telegram-plugin/flood-circuit-breaker.ts
299
+ import {
300
+ existsSync,
301
+ readFileSync as readFileSync2,
302
+ writeFileSync as writeFileSync2,
303
+ mkdirSync as mkdirSync2,
304
+ chmodSync as chmodSync2,
305
+ unlinkSync as unlinkSync2,
306
+ renameSync
307
+ } from "node:fs";
308
+ import { dirname as dirname2, join as join3 } from "node:path";
309
+
310
+ // telegram-plugin/flood-429-ledger.ts
311
+ import { readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from "node:fs";
312
+ import { dirname, join as join2 } from "node:path";
313
+ var FLOOD_429_LEDGER_FILE = "429-ledger.json";
314
+ var FLOOD_429_LEDGER_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
315
+ var PENALTY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
316
+ var TRIVIAL_PRESSURE_WINDOW_MS = 24 * 60 * 60 * 1000;
317
+ function flood429LedgerPath(stateDir) {
318
+ return join2(stateDir, FLOOD_429_LEDGER_FILE);
319
+ }
320
+ function parseEpisode(raw) {
321
+ if (typeof raw !== "object" || raw === null)
322
+ return null;
323
+ const r = raw;
324
+ const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : null;
325
+ const firstTs = num(r.firstTs);
326
+ const untilTs = num(r.untilTs);
327
+ const peak = num(r.peakRetryAfterSec);
328
+ if (firstTs === null || untilTs === null || peak === null)
329
+ return null;
330
+ return {
331
+ firstTs,
332
+ lastTs: num(r.lastTs) ?? firstTs,
333
+ peakRetryAfterSec: peak,
334
+ untilTs,
335
+ count: num(r.count) ?? 1
336
+ };
337
+ }
338
+ function readFlood429LedgerResult(path, readFile = (p) => readFileSync(p, "utf-8")) {
339
+ let text;
340
+ try {
341
+ text = readFile(path);
342
+ } catch (err) {
343
+ const code = err?.code;
344
+ if (code === "ENOENT" || code === "ENOTDIR")
345
+ return { status: "absent", episodes: [] };
346
+ return {
347
+ status: "unreadable",
348
+ episodes: [],
349
+ error: `${code ?? "EUNKNOWN"}: ${err?.message ?? String(err)}`
350
+ };
351
+ }
352
+ let parsed;
353
+ try {
354
+ parsed = JSON.parse(text);
355
+ } catch (err) {
356
+ return { status: "corrupt", episodes: [], error: err?.message ?? String(err) };
357
+ }
358
+ if (!Array.isArray(parsed)) {
359
+ return { status: "corrupt", episodes: [], error: "ledger is not a JSON array" };
360
+ }
361
+ const out = [];
362
+ let dropped = 0;
363
+ for (const raw of parsed) {
364
+ const ep = parseEpisode(raw);
365
+ if (ep)
366
+ out.push(ep);
367
+ else
368
+ dropped += 1;
369
+ }
370
+ if (dropped > 0 && out.length === 0) {
371
+ return { status: "corrupt", episodes: [], error: `${dropped} unparseable entr(ies)` };
372
+ }
373
+ return { status: "ok", episodes: out };
374
+ }
375
+
376
+ // telegram-plugin/flood-circuit-breaker.ts
377
+ var blindLoggedAt = new Map;
378
+ var FLOOD_WINDOWS_FILE = "flood-windows.json";
379
+ function floodWindowsPath(stateDir) {
380
+ return join3(stateDir, FLOOD_WINDOWS_FILE);
381
+ }
382
+ var FLOOD_WINDOWS_CORRUPT_SUPPRESS_MS = 5 * 60000;
383
+ function readFloodWindows(path, now, log = (l) => process.stderr.write(l)) {
384
+ if (!existsSync(path))
385
+ return [];
386
+ const failSafe = (why) => {
387
+ log(`telegram gateway: flood-breaker: scoped-windows file ${path} is ${why} \u2014 ` + `failing SAFE: opening a conservative ${FLOOD_WINDOWS_CORRUPT_SUPPRESS_MS}ms global ` + `window rather than booting into a possible open ban with no windows (issue #3106)
388
+ `);
389
+ return [
390
+ {
391
+ scopeKey: "global",
392
+ untilTs: now + FLOOD_WINDOWS_CORRUPT_SUPPRESS_MS,
393
+ retryAfterSrc: `failsafe:${why}`,
394
+ observedAt: now
395
+ }
396
+ ];
397
+ };
398
+ let text;
399
+ try {
400
+ text = readFileSync2(path, "utf-8");
401
+ } catch (err) {
402
+ const code = err?.code;
403
+ if (code === "ENOENT" || code === "ENOTDIR")
404
+ return [];
405
+ return failSafe(`unreadable (${code ?? "error"})`);
406
+ }
407
+ let raw;
408
+ try {
409
+ raw = JSON.parse(text);
410
+ } catch {
411
+ return failSafe("corrupt (invalid JSON)");
412
+ }
413
+ if (!Array.isArray(raw))
414
+ return failSafe("corrupt (not an array)");
415
+ const out = [];
416
+ for (const r of raw) {
417
+ const rec = r;
418
+ if (typeof rec.scopeKey !== "string" || typeof rec.untilTs !== "number")
419
+ continue;
420
+ if (rec.untilTs <= now)
421
+ continue;
422
+ out.push({
423
+ scopeKey: rec.scopeKey,
424
+ untilTs: rec.untilTs,
425
+ retryAfterSrc: typeof rec.retryAfterSrc === "string" ? rec.retryAfterSrc : "unknown",
426
+ observedAt: typeof rec.observedAt === "number" ? rec.observedAt : now,
427
+ ...typeof rec.alertedAt === "number" ? { alertedAt: rec.alertedAt } : {}
428
+ });
429
+ }
430
+ return out;
431
+ }
432
+
433
+ // src/agents/flood-pressure-probe.ts
434
+ var FLOOD_PRESSURE_GRACE_MS = 120000;
435
+ function scopeBlocksCardDelivery(scopeKey) {
436
+ return !scopeKey.startsWith("msg-edit:");
437
+ }
438
+ var NO_PRESSURE = { active: false, reason: "" };
439
+ function resolveTelegramStateDir() {
440
+ return process.env.TELEGRAM_STATE_DIR ?? "/state/agent/telegram";
441
+ }
442
+ function readFloodPressure(now, opts = {}) {
443
+ const stateDir = opts.stateDir ?? resolveTelegramStateDir();
444
+ const graceMs = opts.graceMs ?? FLOOD_PRESSURE_GRACE_MS;
445
+ try {
446
+ const windows = readFloodWindows(floodWindowsPath(stateDir), now - graceMs, () => {});
447
+ for (const w of windows) {
448
+ if (!scopeBlocksCardDelivery(w.scopeKey))
449
+ continue;
450
+ const msLeft = w.untilTs - now;
451
+ return {
452
+ active: true,
453
+ reason: msLeft > 0 ? `open flood window ${w.scopeKey} (${Math.ceil(msLeft / 1000)}s left, src=${w.retryAfterSrc})` : `flood window ${w.scopeKey} closed ${Math.ceil(-msLeft / 1000)}s ago (within ${Math.round(graceMs / 1000)}s grace)`
454
+ };
455
+ }
456
+ const ledger = readFlood429LedgerResult(flood429LedgerPath(stateDir));
457
+ if (ledger.status === "ok") {
458
+ for (const ep of ledger.episodes) {
459
+ const lastEvidenceTs = Math.max(ep.lastTs, ep.untilTs);
460
+ if (lastEvidenceTs >= now - graceMs) {
461
+ return {
462
+ active: true,
463
+ reason: `429 episode ${ep.count}x peak=${ep.peakRetryAfterSec}s last seen ` + `${Math.max(0, Math.round((now - ep.lastTs) / 1000))}s ago ` + `(within ${Math.round(graceMs / 1000)}s grace)`
464
+ };
465
+ }
466
+ }
467
+ }
468
+ return NO_PRESSURE;
469
+ } catch {
470
+ return NO_PRESSURE;
471
+ }
472
+ }
473
+
298
474
  // src/agents/wedge-watchdog.ts
299
475
  var WEDGE_FOOTER_SIGNATURE = /(?=[\s\S]*[Ee]sc(?:ape)?[^\n]*cancel)(?=[\s\S]*(?:to select|to navigate|\u2191\/\u2193))/;
300
476
  var RATE_LIMIT_MENU_SIGNATURE = /(?=[\s\S]*Stop and wait for)(?=[\s\S]*(?:usage credits|Upgrade your plan|\/rate-limit-options))/;
@@ -438,6 +614,8 @@ var DEFAULT_COOLDOWN_MS = 60000;
438
614
  var DEFAULT_CONFIRM_MODAL_POLLS = 3;
439
615
  var DEFAULT_MANIFEST_STALL_POLLS = 60;
440
616
  var DEFAULT_PERMISSION_PROMPT_POLLS = 3;
617
+ var DEFAULT_PERMISSION_CARDLESS_POLLS = 24;
618
+ var DEFAULT_PERMISSION_FLOOD_MAX_POLLS = 120;
441
619
  function envInt(name, fallback) {
442
620
  const raw = process.env[name];
443
621
  if (raw === undefined || raw.trim() === "")
@@ -467,6 +645,9 @@ async function runWedgeWatchdog(opts) {
467
645
  const confirmModalPolls = opts.confirmModalPolls ?? envInt("SWITCHROOM_WEDGE_CONFIRM_POLLS", DEFAULT_CONFIRM_MODAL_POLLS);
468
646
  const permissionPromptSignature = opts.permissionPromptSignature === null ? null : opts.permissionPromptSignature ?? PERMISSION_PROMPT_SIGNATURE;
469
647
  const permissionPromptPolls = opts.permissionPromptPolls ?? envInt("SWITCHROOM_WEDGE_PERMISSION_POLLS", DEFAULT_PERMISSION_PROMPT_POLLS);
648
+ const permissionCardlessPolls = Math.max(permissionPromptPolls, opts.permissionCardlessPolls ?? envInt("SWITCHROOM_WEDGE_PERMISSION_CARDLESS_POLLS", DEFAULT_PERMISSION_CARDLESS_POLLS));
649
+ const floodPressure = opts.floodPressure === null ? null : opts.floodPressure ?? ((n) => readFloodPressure(n));
650
+ const permissionFloodMaxPolls = Math.max(permissionCardlessPolls, opts.permissionFloodMaxPolls ?? envInt("SWITCHROOM_WEDGE_PERMISSION_FLOOD_MAX_POLLS", DEFAULT_PERMISSION_FLOOD_MAX_POLLS));
470
651
  const manifestStallSignature = opts.manifestStallSignature === null ? null : opts.manifestStallSignature ?? MANIFESTING_SIGNATURE;
471
652
  const manifestStallPolls = opts.manifestStallPolls ?? envInt("SWITCHROOM_WEDGE_MANIFEST_POLLS", DEFAULT_MANIFEST_STALL_POLLS);
472
653
  const requestRestart = opts.requestRestart;
@@ -485,6 +666,8 @@ async function runWedgeWatchdog(opts) {
485
666
  let confirmModalFires = 0;
486
667
  let permissionPromptFires = 0;
487
668
  let permissionPromptDeferrals = 0;
669
+ let permissionPromptFloodHolds = 0;
670
+ let permissionPromptCardlessHolds = 0;
488
671
  let restartEscalations = 0;
489
672
  let polls = 0;
490
673
  let confirmModalPresent = 0;
@@ -493,6 +676,7 @@ async function runWedgeWatchdog(opts) {
493
676
  let lastManifestKey = null;
494
677
  let confirmCooldownUntil = 0;
495
678
  let permissionCooldownUntil = 0;
679
+ let lastPermissionHoldLogAt = Number.NEGATIVE_INFINITY;
496
680
  while (polls < maxPolls) {
497
681
  polls++;
498
682
  let text = "";
@@ -602,17 +786,37 @@ async function runWedgeWatchdog(opts) {
602
786
  permissionPromptDeferrals++;
603
787
  permissionCooldownUntil = now() + cooldownMs;
604
788
  } else {
605
- const reason = queryPendingPermission2 == null ? "card-aware check disabled" : status && status.ok ? "gateway reports no pending permission (card-less prompt)" : `gateway unreachable/no answer within budget${status && !status.ok ? ` (${status.reason})` : ""}`;
606
- console.error(`[wedge-watchdog] ${opts.agentName}: dismissing stuck per-tool ` + `permission prompt (Esc == decline == safe DENY) after ` + `${permissionPromptPresent} polls present ` + `(~${Math.round(permissionPromptPresent * pollIntervalMs / 1000)}s) \u2014 ${reason}`);
607
- try {
608
- send(opts.agentName, ["Escape"]);
609
- } catch (err) {
610
- console.error(`[wedge-watchdog] ${opts.agentName}: send threw: ${err.message}`);
789
+ const held = now();
790
+ const pressure = floodPressure ? floodPressure(held) : null;
791
+ const logHold = (line) => {
792
+ if (held - lastPermissionHoldLogAt < cooldownMs)
793
+ return;
794
+ lastPermissionHoldLogAt = held;
795
+ console.error(line);
796
+ };
797
+ const floodHold = pressure?.active === true && permissionPromptPresent < permissionFloodMaxPolls;
798
+ const cardless = status != null && status.ok && !status.pending;
799
+ const cardlessHold = !floodHold && cardless && permissionPromptPresent < permissionCardlessPolls;
800
+ if (floodHold) {
801
+ permissionPromptFloodHolds++;
802
+ logHold(`[wedge-watchdog] ${opts.agentName}: permission prompt present ` + `${permissionPromptPresent} polls ` + `(~${Math.round(permissionPromptPresent * pollIntervalMs / 1000)}s) but ` + `Telegram is flood-throttling this agent (${pressure.reason}) \u2014 ` + `an approval card may be deferred, NOT sending Esc ` + `(ceiling ${permissionFloodMaxPolls} polls)`);
803
+ } else if (cardlessHold) {
804
+ permissionPromptCardlessHolds++;
805
+ logHold(`[wedge-watchdog] ${opts.agentName}: gateway reports no pending ` + `permission after ${permissionPromptPresent} polls ` + `(~${Math.round(permissionPromptPresent * pollIntervalMs / 1000)}s) \u2014 ` + `holding Esc until the card-less streak of ${permissionCardlessPolls} polls ` + `(~${Math.round(permissionCardlessPolls * pollIntervalMs / 1000)}s), ` + `in case the card is merely late`);
806
+ } else {
807
+ const reason = queryPendingPermission2 == null ? "card-aware check disabled" : status && status.ok ? `gateway reports no pending permission (card-less prompt) for ${permissionCardlessPolls}+ polls` : `gateway unreachable/no answer within budget${status && !status.ok ? ` (${status.reason})` : ""}`;
808
+ const floodNote = pressure?.active ? ` [flood ceiling ${permissionFloodMaxPolls} polls reached while ${pressure.reason}]` : "";
809
+ console.error(`[wedge-watchdog] ${opts.agentName}: dismissing stuck per-tool ` + `permission prompt (Esc == decline == safe DENY) after ` + `${permissionPromptPresent} polls present ` + `(~${Math.round(permissionPromptPresent * pollIntervalMs / 1000)}s) \u2014 ${reason}${floodNote}`);
810
+ try {
811
+ send(opts.agentName, ["Escape"]);
812
+ } catch (err) {
813
+ console.error(`[wedge-watchdog] ${opts.agentName}: send threw: ${err.message}`);
814
+ }
815
+ fires++;
816
+ permissionPromptFires++;
817
+ permissionCooldownUntil = now() + cooldownMs;
818
+ permissionPromptPresent = 0;
611
819
  }
612
- fires++;
613
- permissionPromptFires++;
614
- permissionCooldownUntil = now() + cooldownMs;
615
- permissionPromptPresent = 0;
616
820
  }
617
821
  }
618
822
  } else if (isConfirmModal) {
@@ -667,6 +871,8 @@ async function runWedgeWatchdog(opts) {
667
871
  confirmModalFires,
668
872
  permissionPromptFires,
669
873
  permissionPromptDeferrals,
874
+ permissionPromptFloodHolds,
875
+ permissionPromptCardlessHolds,
670
876
  restartEscalations,
671
877
  polls,
672
878
  reason: "max-polls"
@@ -675,10 +881,10 @@ async function runWedgeWatchdog(opts) {
675
881
 
676
882
  // src/agents/rate-limit-signal.ts
677
883
  import { createConnection as createConnection2 } from "node:net";
678
- import { join as join2 } from "node:path";
884
+ import { join as join4 } from "node:path";
679
885
  function resolveGatewaySocketPath2() {
680
886
  const stateDir = process.env.TELEGRAM_STATE_DIR ?? "/state/agent/telegram";
681
- return process.env.SWITCHROOM_GATEWAY_SOCKET ?? join2(stateDir, "gateway.sock");
887
+ return process.env.SWITCHROOM_GATEWAY_SOCKET ?? join4(stateDir, "gateway.sock");
682
888
  }
683
889
  function signalQuotaWall(agentName, resetAt, opts = {}) {
684
890
  const socketPath = opts.socketPath ?? resolveGatewaySocketPath2();
@@ -744,7 +950,7 @@ function signalQuotaWall(agentName, resetAt, opts = {}) {
744
950
  import * as net from "node:net";
745
951
  import { homedir } from "node:os";
746
952
  import { randomUUID as randomUUID2 } from "node:crypto";
747
- import { join as join3 } from "node:path";
953
+ import { join as join5 } from "node:path";
748
954
 
749
955
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
750
956
  var exports_external = {};
@@ -5031,7 +5237,7 @@ function reviveDate(v) {
5031
5237
  return Number.isNaN(d.getTime()) ? null : d;
5032
5238
  }
5033
5239
  function operatorSocketPath(home = homedir()) {
5034
- return join3(home, ".switchroom", "state", "auth-broker-operator", "sock");
5240
+ return join5(home, ".switchroom", "state", "auth-broker-operator", "sock");
5035
5241
  }
5036
5242
  function resolveAuthBrokerSocketPath(opts) {
5037
5243
  if (opts?.socket)
@@ -5406,8 +5612,9 @@ async function main() {
5406
5612
  const rateLimitDetect = process.env.SWITCHROOM_RATE_LIMIT_DETECT !== "0";
5407
5613
  const overageSelect = rateLimitDetect && process.env.SWITCHROOM_RATE_LIMIT_OVERAGE !== "0";
5408
5614
  const permissionCardAware = process.env.SWITCHROOM_PERMISSION_CARD_AWARE !== "0";
5615
+ const permissionFloodAware = process.env.SWITCHROOM_WEDGE_FLOOD_AWARE !== "0";
5409
5616
  try {
5410
- console.error(`[autoaccept-poll] ${agentName}: entering wedge-watchdog (continuous)` + (rateLimitDetect ? " +rate-limit-detect" : " (rate-limit-detect OFF)") + (overageSelect ? " +overage-carveout" : "") + (permissionCardAware ? " +permission-card-aware" : " (permission-card-aware OFF)"));
5617
+ console.error(`[autoaccept-poll] ${agentName}: entering wedge-watchdog (continuous)` + (rateLimitDetect ? " +rate-limit-detect" : " (rate-limit-detect OFF)") + (overageSelect ? " +overage-carveout" : "") + (permissionCardAware ? " +permission-card-aware" : " (permission-card-aware OFF)") + (permissionFloodAware ? " +permission-flood-aware" : " (permission-flood-aware OFF)"));
5411
5618
  const res = await runWedgeWatchdog({
5412
5619
  agentName,
5413
5620
  rateLimitSignature: rateLimitDetect ? undefined : null,
@@ -5416,9 +5623,10 @@ async function main() {
5416
5623
  } : undefined,
5417
5624
  overageDecision: overageSelect ? () => queryActiveOverageServing() : undefined,
5418
5625
  requestRestart: requestWedgeRestart,
5419
- queryPendingPermission: permissionCardAware ? undefined : null
5626
+ queryPendingPermission: permissionCardAware ? undefined : null,
5627
+ floodPressure: permissionFloodAware ? undefined : null
5420
5628
  });
5421
- console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog returned reason=${res.reason} fires=${res.fires} rateLimitFires=${res.rateLimitFires} overageCreditSelections=${res.overageCreditSelections} confirmModalFires=${res.confirmModalFires} permissionPromptFires=${res.permissionPromptFires} permissionPromptDeferrals=${res.permissionPromptDeferrals} restartEscalations=${res.restartEscalations}`);
5629
+ console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog returned reason=${res.reason} fires=${res.fires} rateLimitFires=${res.rateLimitFires} overageCreditSelections=${res.overageCreditSelections} confirmModalFires=${res.confirmModalFires} permissionPromptFires=${res.permissionPromptFires} permissionPromptDeferrals=${res.permissionPromptDeferrals} permissionPromptFloodHolds=${res.permissionPromptFloodHolds} permissionPromptCardlessHolds=${res.permissionPromptCardlessHolds} restartEscalations=${res.restartEscalations}`);
5422
5630
  } catch (err) {
5423
5631
  console.error(`[autoaccept-poll] ${agentName}: wedge-watchdog unexpected throw: ${err.message}`);
5424
5632
  }
@@ -11718,6 +11718,16 @@ init_zod();
11718
11718
 
11719
11719
  // src/config/schema.ts
11720
11720
  init_zod();
11721
+
11722
+ // src/memory/observation-scopes.ts
11723
+ var OBSERVATION_SCOPES = [
11724
+ "per_tag",
11725
+ "combined",
11726
+ "all_combinations",
11727
+ "shared"
11728
+ ];
11729
+
11730
+ // src/config/schema.ts
11721
11731
  var CodeRepoEntrySchema = exports_external.object({
11722
11732
  name: exports_external.string().describe("Short alias used when claiming (e.g. 'switchroom')"),
11723
11733
  source: exports_external.string().describe("Absolute or home-relative path to the repo (e.g. ~/code/switchroom)"),
@@ -11829,6 +11839,7 @@ var AgentToolsSchema = exports_external.object({
11829
11839
  allow: exports_external.array(exports_external.string()).default([]).describe("Allowed tools (use ['all'] for unrestricted)"),
11830
11840
  deny: exports_external.array(exports_external.string()).default([]).describe("Denied tools (overrides allow)")
11831
11841
  }).optional();
11842
+ var ObservationScopesSchema = exports_external.enum(OBSERVATION_SCOPES).optional().describe("Per-row observation scope stamped on every memory this agent " + `retains. "shared" makes Hindsight's consolidation write the ` + "resulting observations into ONE global untagged scope instead of a " + "scope per tag \u2014 what several agents pooling one bank need so their " + "observations actually merge rather than sitting in parallel " + "per-tag silos. OMITTED BY DEFAULT: unset means the field never goes " + "on the wire and the engine's own default stands, which is the " + "shipped behaviour. Applies to every retain path (Stop hook, " + "sidechain, boot reconcile, queue drain, backfill, session-handoff " + "mirror) and is carried on the queued payload, so a retain that fails " + "now and drains later still lands in this scope. Accepted values: " + `${OBSERVATION_SCOPES.join(", ")}. ` + "Cascade: override (per-agent wins over default).");
11832
11843
  var AgentMemorySchema = exports_external.object({
11833
11844
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
11834
11845
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
@@ -11863,6 +11874,7 @@ var AgentMemorySchema = exports_external.object({
11863
11874
  empathy: exports_external.number().int().min(1).max(5).optional().describe("How much the bank weights emotional/relational context (1-5; engine default 3).")
11864
11875
  }).optional().describe("Personality traits (1-5 each) steering how this bank frames recall, " + "reflect, and observation synthesis \u2014 a coach leans empathy-high, a " + "lawyer/analyst leans skepticism/literalism-high. Maps to the engine's " + "flat `disposition_skepticism`/`_literalism`/`_empathy` fields. " + "Cascade: per-key merge (an agent overrides individual traits and " + "inherits the rest, matching `recall`)."),
11865
11876
  directive_capture_nudge: exports_external.boolean().optional().describe("Deterministic directive-capture nudge (issue #2848 Stage B). When " + "on (switchroom default true \u2014 Stage A measured a ~55% miss rate on " + "durable corrections), the auto-recall hook regex-detects correction " + '/ standing-rule-shaped inbound ("always/never \u2026", "from now on \u2026", ' + `"stop doing \u2026", a stated preference, "that's wrong, it's \u2026") and ` + "appends a terse advisory to the turn's context telling the model to " + "persist the rule with mcp__hindsight__create_directive if it IS " + "durable. Detection is pure regex \u2014 the model does the judgment " + "in-session and calls create_directive itself (no model callsite, no " + "silent hook-side write). Set false to disable per-agent. " + "Cascade: override (per-agent wins over default)."),
11877
+ observation_scopes: ObservationScopesSchema,
11866
11878
  recall: exports_external.object({
11867
11879
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
11868
11880
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
@@ -12106,7 +12118,10 @@ var LiteLLMConfigSchema = exports_external.object({
12106
12118
  admin_key: exports_external.string().optional().describe("LiteLLM master/admin key used at apply time to provision the team + " + "virtual key. Supports a vault reference (e.g. " + "'vault:litellm/master-key') \u2014 resolution happens at apply time via " + "the vault-broker. Never injected into the agent container."),
12107
12119
  team: exports_external.string().optional().describe("LiteLLM team alias the per-agent key is created under. Defaults to " + "'switchroom' (applied in code, not as a schema default)."),
12108
12120
  small_fast_model: exports_external.string().optional().describe("Model id exported as ANTHROPIC_SMALL_FAST_MODEL for the claude CLI's " + "background/fast lane, e.g. 'claude-haiku-4-5-20251001'."),
12109
- tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins).")
12121
+ tags: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Extra key/value metadata tags attached to the provisioned LiteLLM " + "virtual key. Merged per-key across cascade layers (agent wins)."),
12122
+ max_budget: exports_external.number().positive().optional().describe("HARD spend cap in USD for this agent's virtual key over one " + "`budget_duration` window. LiteLLM refuses the request once the key's " + "tracked spend exceeds it, so a runaway loop costs at most this much " + "before it is stopped. Defaults to " + "DEFAULT_KEY_MAX_BUDGET_USD (see src/litellm/budget.ts) \u2014 deliberately " + "conservative; raise it per-agent rather than removing it. Set 0 or " + "omit `budget_duration` at your own risk: an uncapped key is only as " + "bounded as the upstream account balance."),
12123
+ soft_budget: exports_external.number().positive().optional().describe("ADVISORY spend threshold in USD. LiteLLM keeps serving past it and " + "raises a budget alert instead. Must be < max_budget. NOTE: LiteLLM " + "accepts soft_budget only on POST /key/generate (GenerateKeyRequest); " + "UpdateKeyRequest does NOT carry it, so changing this value only takes " + "effect on a key that is (re)generated, not on an existing one."),
12124
+ budget_duration: exports_external.string().regex(/^\d+(s|m|h|d|mo)$/, "budget_duration must be a LiteLLM duration like '30d', '24h', '1mo'").optional().describe("Rolling window the budget resets on, in LiteLLM duration syntax " + "('30d', '24h', '1mo'). Defaults to DEFAULT_KEY_BUDGET_DURATION. " + "WITHOUT a duration LiteLLM treats max_budget as a LIFETIME cap that " + "never resets \u2014 the key silently dies for good once it is hit.")
12110
12125
  }).optional().describe("LiteLLM routing config \u2014 opt-in per-agent virtual-key auto-provisioning " + "+ routing env. Default OFF. See LiteLLMConfigSchema doc for the full flow.");
12111
12126
  var HindsightPerOpLlmSchema = exports_external.object({
12112
12127
  model: exports_external.string().min(1).optional().describe("Per-op model (upstream `HINDSIGHT_API_<OP>_LLM_MODEL`). Absent \u2192 " + "inherit the global `hindsight.llm.model`."),
@@ -12125,7 +12140,7 @@ var HindsightConfigSchema = exports_external.object({
12125
12140
  reflect: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `reflect` LLM op (synthesis / mental-model " + "refresh). Emits `HINDSIGHT_API_REFLECT_LLM_*`. Absent \u2192 uses global."),
12126
12141
  consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
12127
12142
  }).optional().describe("LLM knob for the hindsight container. The flat `provider`/`model` set " + "the global default (backward-compatible); optional `retain`/`reflect`/" + "`consolidation` blocks override individual ops. All fields optional; " + "unset fields fall back to the hard-coded defaults."),
12128
- env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
12143
+ env: exports_external.record(exports_external.union([exports_external.string(), exports_external.number(), exports_external.boolean()])).optional().describe("Operator overrides for switchroom's capability-gated Hindsight " + "performance defaults. Only the keys switchroom actually manages are " + "honoured (`HINDSIGHT_PERF_ENV_KEYS` in " + "src/setup/hindsight-perf-defaults.ts: RERANKER_LOCAL_FP16, " + "RERANKER_LOCAL_BATCH_SIZE, LLM_MAX_CONCURRENT, " + "RETAIN/CONSOLIDATION_LLM_MAX_CONCURRENT, LLM_STRICT_SCHEMA, " + "LLM_MAX_RETRIES, CONSOLIDATION_LLM_PARALLELISM, " + "MAX_OBSERVATIONS_PER_SCOPE, " + "RECALL_MAX_CANDIDATES_PER_SOURCE, LINK_EXPANSION_PER_ENTITY_LIMIT, " + "LINK_EXPANSION_TIMEOUT, LLM_REASONING_EFFORT, " + "RERANKER_LOCAL_BUCKET_BATCHING, RERANKER_MAX_CANDIDATES, " + "RERANKER_LOCAL_MAX_CONCURRENT, RECALL_MAX_CONCURRENT, " + "REFLECT_WALL_TIMEOUT, WORKER_CONSOLIDATION_MAX_SLOTS, " + "WORKER_CONSOLIDATION_SLOT_LIMIT, " + "CONSOLIDATION_MAX_MEMORIES_PER_ROUND, RECENCY_DECAY_FUNCTION, " + "RECENCY_DECAY_HALFLIFE_DAYS \u2014 switchroom defaults recall's recency " + "curve to `exponential` with a 30-day half-life so a fact retained " + "today outranks a stale one, instead of upstream's near-flat " + "linear/365-day window), the override-only keys " + "switchroom manages but ships NO default for " + "(`HINDSIGHT_PERF_OVERRIDE_ONLY_KEYS`: " + "HINDSIGHT_API_WORKER_CONSOLIDATION_BANK_PRIORITY \u2014 a per-deployment " + "`bank-pattern:priority,...` map; unset means upstream's flat " + "created_at FIFO across banks; and " + "HINDSIGHT_CE_DECISIVE_RELATIVE_GAP \u2014 the rollback knob for " + "switchroom's CE-saturation damping patch, a float; >= ~0.65 backs the " + "damping out entirely, unset means the patch's own derived gap; and " + "HINDSIGHT_API_RECENCY_DECAY_LINEAR_WINDOW_DAYS \u2014 only read when the " + "decay function is `linear`, so switchroom ships no default for it but " + "still honours an operator who flips the function back; and " + "HINDSIGHT_API_WORKER_MAX_SLOTS \u2014 the worker poller's TOTAL in-flight " + "task budget, the pool WORKER_CONSOLIDATION_MAX_SLOTS reserves out of; " + "unset means upstream's own default; and " + "HINDSIGHT_API_WORKER_RETAIN_MAX_SLOTS \u2014 the reserved slot FLOOR for the " + "retain (memory write) lane, carved from that same total; unset means " + "upstream's own 0, i.e. no floor and retain competes for the shared " + "pool), plus the " + "embedded-PostgreSQL (pg0) sizing keys switchroom manages in " + "src/setup/hindsight-pg-defaults.ts (`HINDSIGHT_PG_ENV_KEYS`: " + "SWITCHROOM_HINDSIGHT_PG_EFFECTIVE_CACHE_SIZE, " + "SWITCHROOM_HINDSIGHT_PG_SHARED_BUFFERS \u2014 a postgres size string such " + "as `4GB`, or the sentinel `off` to leave pg0's own default for that " + "one knob). A value set here " + "REPLACES switchroom's default and is emitted even when the gating " + "capability is absent, so an operator can always force a knob. Other " + "`HINDSIGHT_API_*` keys are deliberately IGNORED \u2014 a blanket " + "passthrough would collide with the vars startHindsight() derives " + "itself (HINDSIGHT_API_PORT, the retain token/deadline budget).")
12129
12144
  });
12130
12145
  var MicrosoftWorkspaceConfigSchema = exports_external.object({
12131
12146
  microsoft_client_id: exports_external.string().min(1).optional().describe("Microsoft OAuth application (client) ID from Entra portal " + "(literal string or vault reference e.g. " + "'vault:microsoft-oauth-client-id'). OPTIONAL \u2014 omit it to use " + "switchroom's shipped default Microsoft app (zero-config). " + "Set it only to bring your own Entra app (BYO)."),
@@ -12258,6 +12273,7 @@ var profileFields = {
12258
12273
  file: exports_external.boolean().optional(),
12259
12274
  isolation: exports_external.enum(["default", "strict"]).optional(),
12260
12275
  directive_capture_nudge: exports_external.boolean().optional(),
12276
+ observation_scopes: ObservationScopesSchema,
12261
12277
  recall: exports_external.object({
12262
12278
  max_memories: exports_external.number().int().min(0).optional(),
12263
12279
  cache_ttl_secs: exports_external.number().int().min(0).optional(),