switchroom 0.19.26 → 0.19.28
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.
- package/bin/git-agent-attribution-hook.sh +144 -0
- package/dist/agent-scheduler/index.js +60 -2
- package/dist/auth-broker/index.js +244 -13
- package/dist/cli/autoaccept-poll.js +225 -17
- package/dist/cli/notion-write-pretool.mjs +60 -2
- package/dist/cli/switchroom.js +2843 -1220
- package/dist/host-control/main.js +245 -14
- package/dist/vault/approvals/kernel-server.js +242 -13
- package/dist/vault/broker/server.js +242 -13
- package/package.json +7 -2
- package/profiles/_base/cron-session.sh.hbs +8 -0
- package/profiles/_base/start.sh.hbs +175 -15
- package/telegram-plugin/card-layout.ts +328 -0
- package/telegram-plugin/dist/bridge/bridge.js +94 -1
- package/telegram-plugin/dist/gateway/gateway.js +2544 -1182
- package/telegram-plugin/dist/server.js +97 -1
- package/telegram-plugin/edit-flood-fuse.ts +841 -57
- package/telegram-plugin/flood-429-ledger.ts +526 -0
- package/telegram-plugin/flood-circuit-breaker.ts +18 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
- package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
- package/telegram-plugin/gateway/gateway.ts +67 -70
- package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
- package/telegram-plugin/gateway/narrative-lane.ts +14 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
- package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
- package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
- package/telegram-plugin/gateway/status-pin-retarget.ts +72 -36
- package/telegram-plugin/gateway/status-pin-store.ts +58 -9
- package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
- package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
- package/telegram-plugin/llm-error-present.ts +61 -2
- package/telegram-plugin/mcp-credential-failure.ts +459 -0
- package/telegram-plugin/model-unavailable.ts +8 -0
- package/telegram-plugin/operator-events.ts +110 -5
- package/telegram-plugin/outbound-class.ts +81 -0
- package/telegram-plugin/provider-credit.ts +237 -0
- package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
- package/telegram-plugin/send-gate.ts +24 -2
- package/telegram-plugin/status-no-truncate.ts +10 -48
- package/telegram-plugin/status-pin-driver.ts +33 -45
- package/telegram-plugin/status-pin.ts +18 -1
- package/telegram-plugin/tests/card-golden.test.ts +69 -0
- package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +187 -164
- package/telegram-plugin/tests/card-variants.golden.txt +211 -0
- package/telegram-plugin/tests/card-variants.ts +366 -0
- package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +373 -0
- package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
- package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
- package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
- package/telegram-plugin/tests/finalize-callback-flood-policy.test.ts +298 -0
- package/telegram-plugin/tests/finalize-callback.test.ts +41 -8
- package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
- package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
- package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
- package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
- package/telegram-plugin/tests/mcp-credential-failure.test.ts +310 -0
- package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +19 -24
- package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
- package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
- package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
- package/telegram-plugin/tests/status-pin-retarget.test.ts +90 -62
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
- package/telegram-plugin/tests/status-pin-store.test.ts +109 -60
- package/telegram-plugin/tests/status-pin.test.ts +56 -5
- package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +23 -29
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
- package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
- package/telegram-plugin/tool-activity-summary.ts +239 -365
- package/telegram-plugin/uat/assertions.ts +22 -11
- package/telegram-plugin/uat/feed-matcher.test.ts +24 -17
- package/telegram-plugin/worker-activity-feed.ts +105 -47
- package/vendor/hindsight-memory/CLAUDE.md +45 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +433 -11
- package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
- package/vendor/hindsight-memory/scripts/lib/pending.py +193 -28
- package/vendor/hindsight-memory/scripts/recall.py +176 -7
- package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
- package/vendor/hindsight-memory/scripts/tests/test_drain_circuit_breaker.py +401 -0
- package/vendor/hindsight-memory/scripts/tests/test_drain_serialisation.py +286 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
- package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
- package/vendor/hindsight-memory/settings.json +1 -1
- 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
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
}
|
|
@@ -11873,10 +11873,37 @@ var AgentMemorySchema = exports_external.object({
|
|
|
11873
11873
|
request_timeout_seconds: exports_external.number().int().min(1).optional().describe("Per-bank HTTP read timeout, in seconds, for one recall " + "request. Even parallelised, each bank carries its own deadline " + "so ONE hung bank returns empty instead of consuming the shared " + "deadline and starving its siblings. The plugin default is 12 " + "(raised from a hardcoded 8 in #3757, which fired on 96.8% of " + "one agent's own-bank recalls). Switchroom defaults it to the " + "effective `parallel_deadline_seconds` instead \u2014 10 at the " + "shipped ceiling \u2014 because the shared fan-out deadline is " + "already the tighter outer guard, so a per-bank value above it " + "can never bind. An explicitly configured value above the " + "effective deadline is clamped down to it, and the clamp is " + "reported."),
|
|
11874
11874
|
own_bank_min_slots: exports_external.number().int().min(0).optional().describe("Slots inside `max_memories` reserved as a FLOOR for the agent's " + "own bank when recall fans out to more than one bank. The merged " + "set is sorted globally by relevance and head-sliced, which is " + "winner-take-all across banks: when both banks return more " + "candidates than the cap, one bank's score distribution can fill " + "every slot and the agent gets a dossier about its operator with " + "none of its own session memory. A floor, not a quota: at most " + "this many slots, only if the own bank returned that many, and " + "only up to HALF the cap shared with `additional_bank_min_slots` " + "\u2014 the rest is always won on pure relevance, so composition still " + "moves with the scores. Fixes score-based crowd-out only; a " + "timed-out bank returns no candidates and reservation is a no-op " + "there. 0 disables (default). Switchroom-managed agents use 2 " + "against the fleet-deployed cap of 6."),
|
|
11875
11875
|
additional_bank_min_slots: exports_external.number().int().min(0).optional().describe("Slots inside `max_memories` reserved as a FLOOR for the " + "additional (profile / shared / sender) banks. Symmetric with " + "`own_bank_min_slots` \u2014 same floor-not-quota semantics, and the " + "two share the same half-of-cap reservation budget. When they sum " + "above that budget the own-bank floor is honoured first. 0 " + "disables (default). Switchroom-managed agents use 1 against the " + "fleet-deployed cap of 6. Observe `injected_own_bank_count` / " + "`injected_additional_bank_count` via " + "`switchroom memory recall-log`."),
|
|
11876
|
+
min_score: exports_external.number().min(0).optional().describe("Absolute floor on a memory's engine relevance score " + "(`scores.final`) for it to be injected. 0 disables (default, " + "and the shipped fleet behaviour). Exists for one measured " + "failure: when the agent's own bank times out, recall still " + "injects side-bank residue under the banner 'Relevant memories " + "from past conversations' \u2014 98.4% of degraded turns have a best " + "injected score below 0.01, against 28.4% of healthy ones. Six " + "noise memories are worse than none, because the agent cannot " + "tell them apart. Below-floor results are dropped BEFORE " + "rendering, and when the floor empties the set the turn says so " + "rather than going silent. Do NOT read this as a general " + "precision control: `scores.final` is not calibrated across " + "queries, and #3761 measured that an unconditional 0.01 floor " + "empties ~28% of HEALTHY recalls \u2014 which is why " + "`min_score_scope` defaults to degraded turns only. Observe " + "`dropped_below_min_score` via `switchroom memory recall-log`."),
|
|
11877
|
+
min_score_scope: exports_external.enum(["degraded", "all"]).optional().describe('Which turns `min_score` binds on. "degraded" (default) \u2014 only ' + "turns where the agent's OWN bank timed out or was unreachable, " + "the population where a below-floor score actually predicts " + "noise and where the agent already receives the degraded-recall " + 'disclosure. "all" \u2014 every turn; only for an operator who has ' + "measured their own bank's score distribution, since it " + "re-creates the empty-recall failure of #3541 at any floor " + "calibrated on degraded data. No effect while `min_score` is 0."),
|
|
11876
11878
|
types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] \u2014 the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
|
|
11877
11879
|
additional_banks: exports_external.array(exports_external.string()).optional().describe("Extra Hindsight banks to recall from on every turn, merged into " + "the agent's own bank results \u2014 e.g. a shared operator/household " + "profile bank authored via `switchroom memory profile`. Each is " + "recalled with the `request_timeout_seconds` per-bank timeout " + "(defaults to the effective `parallel_deadline_seconds`, 10s at " + "the shipped ceiling) and is non-fatal on failure. Stays " + "within the single tenant: all banks are the operator's data, in " + "the operator's Hindsight instance (see the `single-tenant` " + "invariant). Defaults to [] (no extra banks)."),
|
|
11878
11880
|
sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender \u2192 extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username \u2014 a leading @ is " + "optional \u2014 or numeric user_id), merged " + "into its own results \u2014 so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
|
|
11879
11881
|
skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true \u2014 saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
|
|
11882
|
+
budget: exports_external.enum(["low", "mid", "high"]).optional().describe('How hard Hindsight searches. "low" (switchroom default) = ' + 'vector retrieval only, ~1-2s. "mid" adds the LLM rerank pass ' + "and measured ~5s of hook latency on real fleet turns \u2014 the " + "second-largest contributor to perceived dead air after model " + 'TTFT. "high" is thorough and slower still. Raise it for an ' + "agent whose recall quality matters more than its reply latency " + "(a research or audit role); leave it at low for chat."),
|
|
11883
|
+
max_tokens: exports_external.number().int().min(1).optional().describe("Token budget for the injected memory block. Default 1024. This " + "is the TOKEN bound; `max_memories` is the separate COUNT bound " + "and the tighter of the two wins. Raise it only alongside " + "`max_memories` \u2014 on its own it buys nothing once the count cap " + "binds."),
|
|
11884
|
+
prefer_observations: exports_external.boolean().optional().describe("Bias recall toward the synthesized `observation` tier, " + "backfilling the slots freed by superseded raw facts for denser " + "coverage inside the same budget. Default true. Set false to " + "rank raw `world`/`experience` facts on equal footing \u2014 useful " + "when auditing what the consolidation engine actually stored, or " + "if a bank's observations are stale."),
|
|
11885
|
+
context_turns: exports_external.number().int().min(1).optional().describe("How many recent human turns are composed into the recall query. " + 'Default 2, so a bare follow-up ("and the port?") embeds with ' + "its antecedent instead of recalling on the pronoun alone. 1 = " + "the latest turn only. Raising it costs BM25 terms, which is the " + "real recall cost driver \u2014 `query_max_tokens` still bounds the " + "result, so a large value mostly shifts which terms survive."),
|
|
11886
|
+
roles: exports_external.array(exports_external.string().min(1)).min(1).optional().describe("Transcript roles the multi-turn composition may draw from. " + 'Default ["user", "assistant"]. Set ["user"] to compose ' + "the query from the human's words only \u2014 worth trying when an " + "agent's own verbose replies are dominating the query terms. No " + "effect while `context_turns` is 1."),
|
|
11887
|
+
prompt_preamble: exports_external.string().min(1).optional().describe("The banner rendered above injected memories. The agent reads " + "this line as the instruction for how to treat the block, so it " + "is a behaviour knob, not cosmetics. Default tells the model to " + "prioritise recent memories on conflict and ignore irrelevant " + "ones. Override to tighten that framing for a specialised agent."),
|
|
11888
|
+
tags: exports_external.array(exports_external.string().min(1)).optional().describe("Restrict recall to memories carrying these tags. Default [] = " + "no filter (match everything). This is a HARD filter applied " + "server-side \u2014 a memory without the tags cannot surface at any " + "score \u2014 so it is for a genuinely scoped agent, not for " + "ranking. Use `tag_weights` when you want a preference rather " + "than an exclusion."),
|
|
11889
|
+
tags_match: exports_external.enum(["any", "all", "any_strict", "all_strict"]).optional().describe('How `tags` combine. "any" (default) = at least one; ' + '"all" = every tag. The `_strict` forms additionally require ' + "the memory to actually carry the tags rather than merely rank " + "for them. No effect while `tags` and `tag_groups` are empty."),
|
|
11890
|
+
tag_groups: exports_external.union([
|
|
11891
|
+
exports_external.array(exports_external.array(exports_external.string().min(1))),
|
|
11892
|
+
exports_external.record(exports_external.string(), exports_external.array(exports_external.string().min(1)))
|
|
11893
|
+
]).optional().describe("Tag filtering with grouping \u2014 either an OR-of-ANDs list " + '([["a","b"],["c"]] = (a AND b) OR c) or a named ' + "{group: [tags]} map. Default unset (no grouping). Use when a " + "flat `tags` + `tags_match` cannot express the scope you need."),
|
|
11894
|
+
tag_weights: exports_external.record(exports_external.string(), exports_external.number().min(0)).optional().describe("Per-tag multipliers applied to `scores.final` just before the " + "final sort \u2014 a DEMOTION/PROMOTION, never a drop, so a " + "down-weighted memory still surfaces when it is the only " + "relevant hit. MERGED over switchroom's seed " + '({"sidechain": 0.8}, which ranks delegated sub-agent ' + "process-memories just under first-party ones), so setting one " + "unrelated weight does not silently undo it; pass " + "`sidechain: 1.0` to neutralise the seed. Reach for this when " + "recall_log shows one class of memory crowding the block."),
|
|
11895
|
+
additional_bank_filters: exports_external.record(exports_external.string(), exports_external.object({
|
|
11896
|
+
tags: exports_external.array(exports_external.string().min(1)).optional(),
|
|
11897
|
+
tags_match: exports_external.enum(["any", "all", "any_strict", "all_strict"]).optional(),
|
|
11898
|
+
tag_groups: exports_external.union([
|
|
11899
|
+
exports_external.array(exports_external.array(exports_external.string().min(1))),
|
|
11900
|
+
exports_external.record(exports_external.string(), exports_external.array(exports_external.string().min(1)))
|
|
11901
|
+
]).optional()
|
|
11902
|
+
}).strict()).optional().describe("Per-bank overrides of the tag filters above, keyed by bank id " + "(applies to `additional_banks` AND to sender banks). Default " + "{} = every extra bank inherits the global filters. Use it to " + "scope a shared bank \u2014 e.g. recall only `profile`-tagged " + "memories from the operator's profile bank while leaving the " + "agent's own bank unfiltered."),
|
|
11903
|
+
transcript_fallback: exports_external.boolean().optional().describe("When every bank returns zero results AND no bank hit its " + "deadline, grep the current session's transcript tail for turns " + "matching the query and inject them as a clearly-labelled " + "lower-confidence block. Default true \u2014 it covers the window " + "between an abrupt kill and the next boot reconciliation, where " + "the fact layer was never told about the lost turns. Set false " + "if you never want un-consolidated transcript text in context."),
|
|
11904
|
+
transcript_tail_bytes: exports_external.number().int().min(0).optional().describe("Bytes of the session transcript read from the tail for the " + "multi-turn query composition. Default 262144 (256 KiB), which " + "keeps the per-turn read O(1) on a session log that can grow to " + "many MB. 0 = read the whole file (the pre-bound behaviour, and " + "the rollback lever if a composition ever needs older turns)."),
|
|
11905
|
+
max_query_chars: exports_external.number().int().min(1).optional().describe("Character bound on the composed recall query, applied before " + "`query_max_tokens` shapes it. Default 800. Truncation preserves " + "the latest turn and drops the oldest context first. Lower it " + "for an agent whose turns are long pasted payloads."),
|
|
11906
|
+
parallel: exports_external.boolean().optional().describe("Run the directives fetch and every bank recall concurrently " + "under one shared deadline, so total latency is the SLOWEST slot " + "rather than their SUM. Default true. false restores the serial " + "path \u2014 the rollback lever if the parallel path ever misbehaves; " + "expect multi-bank recall latency to add up."),
|
|
11880
11907
|
topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) \u2192 soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: \u2026' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
|
|
11881
11908
|
}).optional().describe("Auto-recall tuning knobs"),
|
|
11882
11909
|
retain: exports_external.object({
|
|
@@ -12079,7 +12106,10 @@ var LiteLLMConfigSchema = exports_external.object({
|
|
|
12079
12106
|
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."),
|
|
12080
12107
|
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)."),
|
|
12081
12108
|
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'."),
|
|
12082
|
-
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).")
|
|
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)."),
|
|
12110
|
+
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."),
|
|
12111
|
+
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."),
|
|
12112
|
+
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.")
|
|
12083
12113
|
}).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.");
|
|
12084
12114
|
var HindsightPerOpLlmSchema = exports_external.object({
|
|
12085
12115
|
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`."),
|
|
@@ -12089,6 +12119,7 @@ var HindsightPerOpLlmSchema = exports_external.object({
|
|
|
12089
12119
|
context_window: exports_external.number().int().positive().optional().describe("Context window (tokens) of the backend serving THIS op. NOT an " + "upstream env var \u2014 switchroom derives the op's token budget " + "(consolidation batch size / max-completion caps / reflect " + "max-context cap) from it so a single call can never overflow the " + "window. Absent \u2192 inherit " + "`hindsight.llm.context_window`, else a per-provider default " + "(conservative for non-`claude-code` providers, which usually mean " + "a local llama.cpp/Ollama slot; a self-hosted `base_url` \u2014 loopback, " + "RFC1918, `.local`/`.internal` \u2014 forces the conservative default too, " + "regardless of the provider NAME, since the endpoint is where the " + "traffic actually terminates). All three lanes (`retain`, " + "`reflect`, `consolidation`) are budgeted independently.")
|
|
12090
12120
|
}).describe("Per-operation LLM override. Every field optional; an unset field (or " + "an omitted op block) inherits the global `hindsight.llm.*`, which is " + "already the engine's fallback \u2014 switchroom emits only the vars set.");
|
|
12091
12121
|
var HindsightConfigSchema = exports_external.object({
|
|
12122
|
+
gpu: exports_external.boolean().optional().describe("Force GPU passthrough for the hindsight container on (`true`) or off " + "(`false`), overriding host autodetection in BOTH directions. Absent " + "(the default) \u2192 autodetect from the persisted host-capabilities " + "verdict (`~/.switchroom/host-capabilities.json`), which enables " + "`--gpus all` only when that file proves BOTH a GPU and the nvidia " + "container toolkit. Set `true` when that verdict is wrong or unreadable " + "and you know the host has a working toolkit \u2014 switchroom cannot verify " + "it for you, and `docker run --gpus all` hard-fails container create on " + "a host without one. Set `false` to pin the container to CPU on a GPU " + "host. This is also the declarative opt-out for the recreate-time GPU " + "drop guard (`switchroom memory setup --recreate` refuses to silently " + "turn a GPU container into a CPU one). `--gpu`/`--no-gpu` on `memory " + "setup` override this for a single run."),
|
|
12092
12123
|
llm: exports_external.object({
|
|
12093
12124
|
provider: exports_external.string().min(1).optional().describe("Hindsight LLM provider (upstream `HINDSIGHT_API_LLM_PROVIDER`). " + "Defaults to `claude-code` (subscription-honest, broker-fed OAuth). " + "Any litellm-routable provider the upstream image supports is valid. " + "Serves as the GLOBAL default for every op absent a per-op override."),
|
|
12094
12125
|
model: exports_external.string().min(1).optional().describe("Hindsight LLM model (upstream `HINDSIGHT_API_LLM_MODEL`). Defaults " + "to HINDSIGHT_DEFAULT_MODEL. Any model your LiteLLM proxy can route " + "is valid, e.g. `openrouter/z-ai/glm-5.2` when routing through the " + "fleet proxy. With provider=claude-code this value is ALSO exported " + "as `ANTHROPIC_MODEL` to the claude subprocess. Serves as the GLOBAL " + "default for every op absent a per-op override."),
|
|
@@ -12097,7 +12128,7 @@ var HindsightConfigSchema = exports_external.object({
|
|
|
12097
12128
|
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."),
|
|
12098
12129
|
consolidation: HindsightPerOpLlmSchema.optional().describe("Per-op override for the `consolidation` LLM op (background memory " + "merge). Emits `HINDSIGHT_API_CONSOLIDATION_LLM_*`. Absent \u2192 global.")
|
|
12099
12130
|
}).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."),
|
|
12100
|
-
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).")
|
|
12131
|
+
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), 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).")
|
|
12101
12132
|
});
|
|
12102
12133
|
var MicrosoftWorkspaceConfigSchema = exports_external.object({
|
|
12103
12134
|
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)."),
|
|
@@ -12240,6 +12271,33 @@ var profileFields = {
|
|
|
12240
12271
|
request_timeout_seconds: exports_external.number().int().min(1).optional(),
|
|
12241
12272
|
own_bank_min_slots: exports_external.number().int().min(0).optional(),
|
|
12242
12273
|
additional_bank_min_slots: exports_external.number().int().min(0).optional(),
|
|
12274
|
+
min_score: exports_external.number().min(0).optional(),
|
|
12275
|
+
min_score_scope: exports_external.enum(["degraded", "all"]).optional(),
|
|
12276
|
+
budget: exports_external.enum(["low", "mid", "high"]).optional(),
|
|
12277
|
+
max_tokens: exports_external.number().int().min(1).optional(),
|
|
12278
|
+
prefer_observations: exports_external.boolean().optional(),
|
|
12279
|
+
context_turns: exports_external.number().int().min(1).optional(),
|
|
12280
|
+
roles: exports_external.array(exports_external.string().min(1)).min(1).optional(),
|
|
12281
|
+
prompt_preamble: exports_external.string().min(1).optional(),
|
|
12282
|
+
tags: exports_external.array(exports_external.string().min(1)).optional(),
|
|
12283
|
+
tags_match: exports_external.enum(["any", "all", "any_strict", "all_strict"]).optional(),
|
|
12284
|
+
tag_groups: exports_external.union([
|
|
12285
|
+
exports_external.array(exports_external.array(exports_external.string().min(1))),
|
|
12286
|
+
exports_external.record(exports_external.string(), exports_external.array(exports_external.string().min(1)))
|
|
12287
|
+
]).optional(),
|
|
12288
|
+
tag_weights: exports_external.record(exports_external.string(), exports_external.number().min(0)).optional(),
|
|
12289
|
+
additional_bank_filters: exports_external.record(exports_external.string(), exports_external.object({
|
|
12290
|
+
tags: exports_external.array(exports_external.string().min(1)).optional(),
|
|
12291
|
+
tags_match: exports_external.enum(["any", "all", "any_strict", "all_strict"]).optional(),
|
|
12292
|
+
tag_groups: exports_external.union([
|
|
12293
|
+
exports_external.array(exports_external.array(exports_external.string().min(1))),
|
|
12294
|
+
exports_external.record(exports_external.string(), exports_external.array(exports_external.string().min(1)))
|
|
12295
|
+
]).optional()
|
|
12296
|
+
}).strict()).optional(),
|
|
12297
|
+
transcript_fallback: exports_external.boolean().optional(),
|
|
12298
|
+
transcript_tail_bytes: exports_external.number().int().min(0).optional(),
|
|
12299
|
+
max_query_chars: exports_external.number().int().min(1).optional(),
|
|
12300
|
+
parallel: exports_external.boolean().optional(),
|
|
12243
12301
|
additional_banks: exports_external.array(exports_external.string()).optional(),
|
|
12244
12302
|
sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional()
|
|
12245
12303
|
}).optional()
|