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.
- package/dist/agent-scheduler/index.js +16 -2
- package/dist/auth-broker/index.js +141 -8
- package/dist/cli/autoaccept-poll.js +225 -17
- package/dist/cli/notion-write-pretool.mjs +18 -2
- package/dist/cli/switchroom.js +864 -43
- package/dist/host-control/main.js +144 -9
- package/dist/vault/approvals/kernel-server.js +146 -9
- package/dist/vault/broker/server.js +146 -9
- package/package.json +3 -2
- package/profiles/_base/start.sh.hbs +79 -15
- package/telegram-plugin/dist/bridge/bridge.js +1 -0
- package/telegram-plugin/dist/gateway/gateway.js +585 -50
- package/telegram-plugin/dist/server.js +1 -0
- package/telegram-plugin/edit-flood-fuse.ts +230 -27
- package/telegram-plugin/gateway/callback-query-handlers.ts +6 -0
- package/telegram-plugin/gateway/gateway.ts +9 -2
- package/telegram-plugin/gateway/mcp-failure-hook.ts +74 -0
- package/telegram-plugin/inline-keyboard-callbacks.ts +202 -21
- package/telegram-plugin/mcp-credential-failure.ts +459 -0
- package/telegram-plugin/operator-events.ts +38 -0
- package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +58 -1
- package/telegram-plugin/tests/edit-flood-fuse-reply-reserve.test.ts +340 -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/mcp-credential-failure.test.ts +310 -0
- package/vendor/hindsight-memory/CHANGELOG.md +50 -0
- package/vendor/hindsight-memory/scripts/backfill_transcripts.py +1 -0
- package/vendor/hindsight-memory/scripts/drain_pending.py +437 -11
- package/vendor/hindsight-memory/scripts/lib/client.py +14 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +91 -0
- package/vendor/hindsight-memory/scripts/lib/pending.py +199 -28
- package/vendor/hindsight-memory/scripts/reconcile_tail.py +1 -0
- package/vendor/hindsight-memory/scripts/retain.py +41 -1
- package/vendor/hindsight-memory/scripts/subagent_retain.py +1 -0
- package/vendor/hindsight-memory/scripts/tests/test_backfill.py +23 -1
- package/vendor/hindsight-memory/scripts/tests/test_backfill_from_logs.py +5 -1
- 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_observation_scopes.py +325 -0
- package/vendor/hindsight-memory/scripts/tests/test_pending_drops.py +817 -8
- package/vendor/hindsight-memory/scripts/tests/test_reconcile_durability.py +160 -1
- package/vendor/hindsight-memory/scripts/tests/test_subagent_retain.py +40 -2
- 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
|
}
|
|
@@ -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(),
|