switchroom 0.21.18 → 0.21.20
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/cli/switchroom.js +247 -3
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/skills/switchroom-release/SKILL.md +6 -1
- package/telegram-plugin/connection-drop.ts +83 -0
- package/telegram-plugin/dist/bridge/bridge.js +41 -2
- package/telegram-plugin/dist/gateway/gateway.js +48 -6
- package/telegram-plugin/dist/server.js +46 -3
- package/telegram-plugin/llm-error-present.ts +25 -0
- package/telegram-plugin/session-tail.ts +38 -1
- package/telegram-plugin/tests/llm-error-present.test.ts +183 -0
- package/vendor/hindsight-memory/.claude-plugin/plugin.json +1 -1
- package/vendor/hindsight-memory/CHANGELOG.md +20 -0
- package/vendor/hindsight-memory/scripts/lib/recall_buffer.py +26 -3
- package/vendor/hindsight-memory/scripts/prefetch.py +40 -6
- package/vendor/hindsight-memory/scripts/recall.py +219 -18
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_invalidation.py +7 -2
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_pipeline.py +98 -7
- package/vendor/hindsight-memory/scripts/tests/test_prefetch_topic_guard.py +279 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_buffer_join.py +74 -1
package/dist/cli/switchroom.js
CHANGED
|
@@ -2120,7 +2120,7 @@ var init_esm = __esm(() => {
|
|
|
2120
2120
|
});
|
|
2121
2121
|
|
|
2122
2122
|
// src/build-info.ts
|
|
2123
|
-
var VERSION = "0.21.
|
|
2123
|
+
var VERSION = "0.21.20", COMMIT_SHA = "fa05da22", COMMIT_DATE = "2026-08-18T20:00:16Z";
|
|
2124
2124
|
|
|
2125
2125
|
// src/cli/resolve-version.ts
|
|
2126
2126
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -21340,6 +21340,60 @@ function renderHindsightHooksOverrides(raw, tunables) {
|
|
|
21340
21340
|
return JSON.stringify(root, null, 2) + `
|
|
21341
21341
|
`;
|
|
21342
21342
|
}
|
|
21343
|
+
function readHooksPrefetchAsyncTimeout(raw) {
|
|
21344
|
+
const absent = { present: false, async: false, timeout: null };
|
|
21345
|
+
let parsed;
|
|
21346
|
+
try {
|
|
21347
|
+
parsed = JSON.parse(raw);
|
|
21348
|
+
} catch {
|
|
21349
|
+
return absent;
|
|
21350
|
+
}
|
|
21351
|
+
const hooks = parsed?.hooks;
|
|
21352
|
+
if (hooks == null || typeof hooks !== "object")
|
|
21353
|
+
return absent;
|
|
21354
|
+
const matchers = hooks.Stop;
|
|
21355
|
+
if (!Array.isArray(matchers))
|
|
21356
|
+
return absent;
|
|
21357
|
+
for (const matcher of matchers) {
|
|
21358
|
+
const inner = matcher?.hooks;
|
|
21359
|
+
if (!Array.isArray(inner))
|
|
21360
|
+
continue;
|
|
21361
|
+
for (const hook of inner) {
|
|
21362
|
+
const h = hook;
|
|
21363
|
+
if (h == null || typeof h.command !== "string")
|
|
21364
|
+
continue;
|
|
21365
|
+
if (!h.command.includes(PREFETCH_HOOK_COMMAND_MARKER))
|
|
21366
|
+
continue;
|
|
21367
|
+
return {
|
|
21368
|
+
present: true,
|
|
21369
|
+
async: h.async === true,
|
|
21370
|
+
timeout: typeof h.timeout === "number" ? h.timeout : null
|
|
21371
|
+
};
|
|
21372
|
+
}
|
|
21373
|
+
}
|
|
21374
|
+
return absent;
|
|
21375
|
+
}
|
|
21376
|
+
function validatePrefetchAsyncTimeout(shape, prefetchRecallTimeoutSeconds) {
|
|
21377
|
+
const problems = [];
|
|
21378
|
+
if (!shape.present) {
|
|
21379
|
+
problems.push("hooks/hooks.json registers no Stop hook for prefetch.py (the async " + "recall-prefetch producer)");
|
|
21380
|
+
return problems;
|
|
21381
|
+
}
|
|
21382
|
+
if (!shape.async) {
|
|
21383
|
+
problems.push('the prefetch.py Stop hook is not marked `"async": true` \u2014 a synchronous ' + "prefetch blocks every turn's completion for up to its timeout");
|
|
21384
|
+
}
|
|
21385
|
+
if (shape.timeout === null || shape.timeout <= 0) {
|
|
21386
|
+
problems.push("the prefetch.py Stop hook has no positive `timeout` (async ceiling) \u2014 a " + "wedged producer would never be reaped");
|
|
21387
|
+
return problems;
|
|
21388
|
+
}
|
|
21389
|
+
if (shape.timeout > MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS) {
|
|
21390
|
+
problems.push(`the prefetch.py async ceiling is ${shape.timeout}s, above the ` + `${MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS}s maximum \u2014 a wedged producer holds a ` + `background slot for that long`);
|
|
21391
|
+
}
|
|
21392
|
+
if (prefetchRecallTimeoutSeconds >= shape.timeout) {
|
|
21393
|
+
problems.push(`memoryPrefetchTimeoutSeconds ${prefetchRecallTimeoutSeconds}s is >= the ` + `${shape.timeout}s async hook ceiling \u2014 the producer's own recall can outlive ` + `its hook and be SIGKILLed mid buffer-write, leaving a torn buffer`);
|
|
21394
|
+
}
|
|
21395
|
+
return problems;
|
|
21396
|
+
}
|
|
21343
21397
|
function readHooksRecallTimeout(raw) {
|
|
21344
21398
|
let parsed;
|
|
21345
21399
|
try {
|
|
@@ -21368,7 +21422,7 @@ function readHooksRecallTimeout(raw) {
|
|
|
21368
21422
|
}
|
|
21369
21423
|
return null;
|
|
21370
21424
|
}
|
|
21371
|
-
var DEFAULT_RECALL_HOOK_TIMEOUT_SECONDS = 12, DEFAULT_RECALL_MAX_MEMORIES = 8, RECALL_DEADLINE_HEADROOM_SECONDS = 2, MIN_RECALL_HOOK_TIMEOUT_SECONDS, DEFAULT_RECALL_REQUEST_TIMEOUT_SECONDS = 12, MANAGED_HOOK_EVENT = "UserPromptSubmit", RECALL_HOOK_COMMAND_MARKER = "recall.py";
|
|
21425
|
+
var DEFAULT_RECALL_HOOK_TIMEOUT_SECONDS = 12, DEFAULT_RECALL_MAX_MEMORIES = 8, RECALL_DEADLINE_HEADROOM_SECONDS = 2, MIN_RECALL_HOOK_TIMEOUT_SECONDS, DEFAULT_RECALL_REQUEST_TIMEOUT_SECONDS = 12, MANAGED_HOOK_EVENT = "UserPromptSubmit", RECALL_HOOK_COMMAND_MARKER = "recall.py", PREFETCH_HOOK_COMMAND_MARKER = "prefetch.py", MAX_PREFETCH_ASYNC_TIMEOUT_SECONDS = 30;
|
|
21372
21426
|
var init_hindsight_recall_tunables = __esm(() => {
|
|
21373
21427
|
init_hindsight_recall_passthrough();
|
|
21374
21428
|
MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
|
|
@@ -28559,6 +28613,25 @@ class DirectiveAdmin {
|
|
|
28559
28613
|
});
|
|
28560
28614
|
return `Deactivated directive '${target.name}' (id ${target.id}) in bank ` + `'${this.opts.bankId}', tagged ${args.tag}.`;
|
|
28561
28615
|
}
|
|
28616
|
+
async deactivateAllActiveByName(args) {
|
|
28617
|
+
const all = await this.list();
|
|
28618
|
+
const named = all.filter((d) => d.name === args.name);
|
|
28619
|
+
if (named.length === 0) {
|
|
28620
|
+
const known = all.map((d) => d.name).sort().join(", ");
|
|
28621
|
+
throw new Error(`no directive named '${args.name}' in bank '${this.opts.bankId}' ` + `(the directive to deactivate). Known directives: ${known || "(none)"}`);
|
|
28622
|
+
}
|
|
28623
|
+
const active = named.filter((d) => d.is_active !== false);
|
|
28624
|
+
const alreadyInactive = named.filter((d) => d.is_active === false).map((d) => ({ id: d.id, name: d.name, priority: d.priority }));
|
|
28625
|
+
for (const d of active)
|
|
28626
|
+
this.refuseIfRulesBlock(d);
|
|
28627
|
+
const deactivated = [];
|
|
28628
|
+
for (const d of active) {
|
|
28629
|
+
if (!args.dryRun)
|
|
28630
|
+
await this.deactivateResolved(all, d);
|
|
28631
|
+
deactivated.push({ id: d.id, name: d.name, priority: d.priority });
|
|
28632
|
+
}
|
|
28633
|
+
return { deactivated, alreadyInactive, dryRun: args.dryRun ?? false };
|
|
28634
|
+
}
|
|
28562
28635
|
async reactivate(args) {
|
|
28563
28636
|
const all = await this.list();
|
|
28564
28637
|
const target = this.resolve(all, args.name, "the directive to reactivate");
|
|
@@ -30482,16 +30555,25 @@ function resolveHindsightVendorResolution() {
|
|
|
30482
30555
|
execPath: process.execPath
|
|
30483
30556
|
});
|
|
30484
30557
|
}
|
|
30558
|
+
function removeVendoredHindsightPlugin(agentDir) {
|
|
30559
|
+
const destPath = join15(agentDir, ".claude", "plugins", "hindsight-memory");
|
|
30560
|
+
if (existsSync20(destPath)) {
|
|
30561
|
+
rmSync7(destPath, { recursive: true, force: true });
|
|
30562
|
+
}
|
|
30563
|
+
}
|
|
30485
30564
|
function installHindsightPlugin(agentName, agentDir, switchroomConfig, resolvedAgentConfig) {
|
|
30486
30565
|
if (!switchroomConfig)
|
|
30487
30566
|
return null;
|
|
30488
30567
|
const memory = switchroomConfig.memory;
|
|
30489
|
-
if (!isHindsightEnabled(switchroomConfig))
|
|
30568
|
+
if (!isHindsightEnabled(switchroomConfig)) {
|
|
30569
|
+
removeVendoredHindsightPlugin(agentDir);
|
|
30490
30570
|
return null;
|
|
30571
|
+
}
|
|
30491
30572
|
if (!memory)
|
|
30492
30573
|
return null;
|
|
30493
30574
|
const agentMemory = switchroomConfig.agents[agentName]?.memory;
|
|
30494
30575
|
if (resolveHindsightAutoRecall(switchroomConfig, agentName, resolvedAgentConfig) === false) {
|
|
30576
|
+
removeVendoredHindsightPlugin(agentDir);
|
|
30495
30577
|
return null;
|
|
30496
30578
|
}
|
|
30497
30579
|
const vendorResolution = resolveHindsightVendorResolution();
|
|
@@ -63701,6 +63783,135 @@ function detectHindsightRecallTunableDrift(name, agentConfig, agentDir, config)
|
|
|
63701
63783
|
}
|
|
63702
63784
|
return findings;
|
|
63703
63785
|
}
|
|
63786
|
+
function detectPrefetchAsyncTimeoutDrift(name, agentConfig, agentDir, config) {
|
|
63787
|
+
if (!isHindsightEnabled(config))
|
|
63788
|
+
return [];
|
|
63789
|
+
const resolved = resolveAgentConfig(config.defaults, config.profiles, agentConfig);
|
|
63790
|
+
if (resolved.memory?.auto_recall === false)
|
|
63791
|
+
return [];
|
|
63792
|
+
const pluginDir = join66(agentDir, ".claude", "plugins", "hindsight-memory");
|
|
63793
|
+
if (!existsSync69(pluginDir))
|
|
63794
|
+
return [];
|
|
63795
|
+
const settingsPath = join66(pluginDir, "settings.json");
|
|
63796
|
+
if (!existsSync69(settingsPath))
|
|
63797
|
+
return [];
|
|
63798
|
+
let settings = null;
|
|
63799
|
+
try {
|
|
63800
|
+
settings = JSON.parse(readFileSync62(settingsPath, "utf-8"));
|
|
63801
|
+
} catch {
|
|
63802
|
+
return [];
|
|
63803
|
+
}
|
|
63804
|
+
if (settings?.memoryPrefetchEnabled !== true)
|
|
63805
|
+
return [];
|
|
63806
|
+
const hooksPath = join66(pluginDir, "hooks", "hooks.json");
|
|
63807
|
+
if (!existsSync69(hooksPath))
|
|
63808
|
+
return [];
|
|
63809
|
+
let shape;
|
|
63810
|
+
try {
|
|
63811
|
+
shape = readHooksPrefetchAsyncTimeout(readFileSync62(hooksPath, "utf-8"));
|
|
63812
|
+
} catch {
|
|
63813
|
+
return [];
|
|
63814
|
+
}
|
|
63815
|
+
const rawPrefetchTimeout = settings.memoryPrefetchTimeoutSeconds;
|
|
63816
|
+
const prefetchRecallTimeout = typeof rawPrefetchTimeout === "number" && Number.isFinite(rawPrefetchTimeout) && rawPrefetchTimeout > 0 ? rawPrefetchTimeout : 5;
|
|
63817
|
+
const problems = validatePrefetchAsyncTimeout(shape, prefetchRecallTimeout);
|
|
63818
|
+
if (problems.length === 0)
|
|
63819
|
+
return [];
|
|
63820
|
+
return [
|
|
63821
|
+
{
|
|
63822
|
+
surface: "memory-prefetch",
|
|
63823
|
+
agent: name,
|
|
63824
|
+
detail: `async recall-prefetch (memoryPrefetchEnabled) is ON but its async-timeout ` + `config is unsafe: ${problems.join("; ")}`,
|
|
63825
|
+
fix: "Re-stamp the plugin (`switchroom apply`) so hooks.json carries the " + '`prefetch.py` Stop hook with `"async": true` and a timeout above ' + "memoryPrefetchTimeoutSeconds, or set memoryPrefetchEnabled off until the " + "async ceiling is corrected \u2014 a wedged prefetch either blocks the turn or " + "is killed mid buffer-write."
|
|
63826
|
+
}
|
|
63827
|
+
];
|
|
63828
|
+
}
|
|
63829
|
+
function hashPluginScriptsTree(scriptsRoot) {
|
|
63830
|
+
if (!existsSync69(scriptsRoot))
|
|
63831
|
+
return null;
|
|
63832
|
+
const out = new Map;
|
|
63833
|
+
const skipDir = (n) => n === "__pycache__";
|
|
63834
|
+
const skipFile = (n) => n.endsWith(".pyc") || n.includes(".bak");
|
|
63835
|
+
const walk = (dir, rel) => {
|
|
63836
|
+
let entries;
|
|
63837
|
+
try {
|
|
63838
|
+
entries = readdirSync27(dir, { withFileTypes: true });
|
|
63839
|
+
} catch {
|
|
63840
|
+
return;
|
|
63841
|
+
}
|
|
63842
|
+
for (const ent of entries) {
|
|
63843
|
+
const childRel = rel === "" ? ent.name : `${rel}/${ent.name}`;
|
|
63844
|
+
const childAbs = join66(dir, ent.name);
|
|
63845
|
+
if (ent.isDirectory()) {
|
|
63846
|
+
if (skipDir(ent.name))
|
|
63847
|
+
continue;
|
|
63848
|
+
walk(childAbs, childRel);
|
|
63849
|
+
} else if (ent.isFile()) {
|
|
63850
|
+
if (skipFile(ent.name))
|
|
63851
|
+
continue;
|
|
63852
|
+
try {
|
|
63853
|
+
out.set(childRel, createHash13("sha256").update(readFileSync62(childAbs)).digest("hex"));
|
|
63854
|
+
} catch {
|
|
63855
|
+
out.set(childRel, "unreadable");
|
|
63856
|
+
}
|
|
63857
|
+
}
|
|
63858
|
+
}
|
|
63859
|
+
};
|
|
63860
|
+
walk(scriptsRoot, "");
|
|
63861
|
+
return out;
|
|
63862
|
+
}
|
|
63863
|
+
function detectHindsightPluginTreeDrift(name, agentDir) {
|
|
63864
|
+
const pluginDir = join66(agentDir, ".claude", "plugins", "hindsight-memory");
|
|
63865
|
+
if (!existsSync69(pluginDir))
|
|
63866
|
+
return [];
|
|
63867
|
+
const releaseResolution = resolveHindsightVendorResolution();
|
|
63868
|
+
if (releaseResolution.path === null) {
|
|
63869
|
+
return [];
|
|
63870
|
+
}
|
|
63871
|
+
const releaseScripts = join66(releaseResolution.path, "scripts");
|
|
63872
|
+
const deployedScripts = join66(pluginDir, "scripts");
|
|
63873
|
+
const releaseHashes = hashPluginScriptsTree(releaseScripts);
|
|
63874
|
+
if (releaseHashes === null || releaseHashes.size === 0) {
|
|
63875
|
+
return [];
|
|
63876
|
+
}
|
|
63877
|
+
const deployedHashes = hashPluginScriptsTree(deployedScripts) ?? new Map;
|
|
63878
|
+
const missing = [];
|
|
63879
|
+
const changed = [];
|
|
63880
|
+
const extra = [];
|
|
63881
|
+
for (const [rel, hash2] of releaseHashes) {
|
|
63882
|
+
const got = deployedHashes.get(rel);
|
|
63883
|
+
if (got === undefined)
|
|
63884
|
+
missing.push(rel);
|
|
63885
|
+
else if (got !== hash2)
|
|
63886
|
+
changed.push(rel);
|
|
63887
|
+
}
|
|
63888
|
+
for (const rel of deployedHashes.keys()) {
|
|
63889
|
+
if (!releaseHashes.has(rel))
|
|
63890
|
+
extra.push(rel);
|
|
63891
|
+
}
|
|
63892
|
+
if (missing.length === 0 && changed.length === 0 && extra.length === 0) {
|
|
63893
|
+
return [];
|
|
63894
|
+
}
|
|
63895
|
+
const parts = [];
|
|
63896
|
+
const summarise = (label, items) => {
|
|
63897
|
+
if (items.length === 0)
|
|
63898
|
+
return;
|
|
63899
|
+
const shown = items.slice(0, 6).sort();
|
|
63900
|
+
const more = items.length > shown.length ? ` (+${items.length - shown.length} more)` : "";
|
|
63901
|
+
parts.push(`${label}: ${shown.join(", ")}${more}`);
|
|
63902
|
+
};
|
|
63903
|
+
summarise("missing", missing);
|
|
63904
|
+
summarise("changed", changed);
|
|
63905
|
+
summarise("stale-extra", extra);
|
|
63906
|
+
return [
|
|
63907
|
+
{
|
|
63908
|
+
surface: "memory-plugin-build",
|
|
63909
|
+
agent: name,
|
|
63910
|
+
detail: `vendored hindsight-memory plugin scripts/ tree does NOT match the ` + `release build (manifest version is not a reliable signal \u2014 it can ` + `sit unchanged across builds): ${parts.join("; ")}`,
|
|
63911
|
+
fix: "Re-vendor the plugin: `switchroom apply` re-copies the release " + "`scripts/` tree into this agent (or removes it entirely when the " + "agent has memory turned off), then restart the agent " + "(`switchroom agent restart " + name + "`). A tree that keeps drifting after apply means the agent's memory " + "config and its on-disk plugin disagree \u2014 check memory.backend / " + "memory.auto_recall for this agent."
|
|
63912
|
+
}
|
|
63913
|
+
];
|
|
63914
|
+
}
|
|
63704
63915
|
function writeDriftReport(agentDir, findings) {
|
|
63705
63916
|
try {
|
|
63706
63917
|
const report = {
|
|
@@ -63724,6 +63935,8 @@ function detectAgentDrift(name, agentConfigRaw, agentsDir, config, configPath, o
|
|
|
63724
63935
|
}));
|
|
63725
63936
|
findings.push(...detectSkillsDrift(name, agentDir));
|
|
63726
63937
|
findings.push(...detectHindsightRecallTunableDrift(name, agentConfig, agentDir, config));
|
|
63938
|
+
findings.push(...detectPrefetchAsyncTimeoutDrift(name, agentConfig, agentDir, config));
|
|
63939
|
+
findings.push(...detectHindsightPluginTreeDrift(name, agentDir));
|
|
63727
63940
|
if (!opts.skipContainerProbes) {
|
|
63728
63941
|
findings.push(...detectHookScriptDrift(name, {
|
|
63729
63942
|
binDir: opts.binDir,
|
|
@@ -110495,6 +110708,37 @@ function registerMemoryDirectiveCommand(memory, program3) {
|
|
|
110495
110708
|
process.exit(1);
|
|
110496
110709
|
}
|
|
110497
110710
|
}));
|
|
110711
|
+
directive.command("deactivate <agent> <name>").description("Deactivate every ACTIVE directive named <name> in <agent>'s bank by " + "flipping is_active=false \u2014 the M3 directive-placement campaign entry " + "point for retiring a guardrail's in-bank copy once it has been " + "relocated to the agent's always-loaded CLAUDE.md. Matches by NAME " + "(mirrors reconcile's 'every ACTIVE directive named <name>' selection); " + "if multiple active copies share the name, all are deactivated. " + "REVERSIBLE \u2014 only is_active is touched, never a delete or a " + "content/priority rewrite, so it can be reactivated. Idempotent: a " + "re-run over already-inactive copies is a clean no-op, not an error. " + "REFUSES any directive carrying the rules-block marker tag (that stays " + "active until an M3-flip removes the marker) \u2014 the whole call is " + "refused if even one active copy carries it, mutating nothing.").option("--dry-run", "Print what WOULD deactivate without mutating anything").option("--json", "Machine-readable output").action(withConfigError(async (agent, name, opts) => {
|
|
110712
|
+
const admin = resolveDirectiveAdmin(program3, agent);
|
|
110713
|
+
try {
|
|
110714
|
+
const result = await admin.deactivateAllActiveByName({
|
|
110715
|
+
name,
|
|
110716
|
+
...opts.dryRun ? { dryRun: true } : {}
|
|
110717
|
+
});
|
|
110718
|
+
if (opts.json) {
|
|
110719
|
+
console.log(JSON.stringify({ ok: true, agent, name, ...result }));
|
|
110720
|
+
return;
|
|
110721
|
+
}
|
|
110722
|
+
const verb = result.dryRun ? "would deactivate" : "deactivated";
|
|
110723
|
+
if (result.deactivated.length === 0) {
|
|
110724
|
+
console.log(source_default.yellow(`\u2022 no-op: no ACTIVE directive named "${name}" in ${agent}'s bank ` + `(${result.alreadyInactive.length} already-inactive ` + `cop${result.alreadyInactive.length === 1 ? "y" : "ies"} left ` + `untouched). Nothing to do.`));
|
|
110725
|
+
return;
|
|
110726
|
+
}
|
|
110727
|
+
for (const d of result.deactivated) {
|
|
110728
|
+
console.log(source_default.green(` ${result.dryRun ? "[dry-run] " : "\u2713 "}${verb} "${d.name}" ` + `(id ${d.id}, prior priority ${d.priority ?? "unset"})`));
|
|
110729
|
+
}
|
|
110730
|
+
const n = result.deactivated.length;
|
|
110731
|
+
console.log(source_default.green(`${result.dryRun ? "[dry-run] " : "\u2713 "}${verb} ${n} directive` + `${n === 1 ? "" : "s"} named "${name}" in ${agent}'s bank` + (result.alreadyInactive.length > 0 ? ` (${result.alreadyInactive.length} already-inactive ` + `cop${result.alreadyInactive.length === 1 ? "y" : "ies"} skipped)` : "") + (result.dryRun ? " \u2014 nothing was mutated" : ". Reverse with reactivate.")));
|
|
110732
|
+
} catch (e) {
|
|
110733
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
110734
|
+
if (opts.json) {
|
|
110735
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
110736
|
+
} else {
|
|
110737
|
+
console.error(source_default.red(`\u2717 ${msg}`));
|
|
110738
|
+
}
|
|
110739
|
+
process.exit(1);
|
|
110740
|
+
}
|
|
110741
|
+
}));
|
|
110498
110742
|
directive.command("mark-rules-block <agent> <id>").description("Stamp the persisted rules-block marker tag on directive <id> in " + "<agent>'s bank \u2014 the SAME DirectiveAdmin.markRulesBlock write path " + "the batch triage executor uses. Once stamped, deactivate_directive " + "(and every other DirectiveAdmin deactivation path) refuses this " + "directive unconditionally until an M3-flip action removes the " + "marker. This is the real entry point the mental-model-curator " + "skill's interactive triage pass calls for every directive it " + "classifies rules-block, BEFORE presenting the card \u2014 that skill " + "has no other write path to this marker (no MCP tool exposes it), " + "so without this call the code-level refusal never actually " + "arms itself on the interactive path (PR #4760 review follow-up).").option("--json", "Machine-readable output").action(withConfigError(async (agent, id, opts) => {
|
|
110499
110743
|
const selfAgent = process.env.SWITCHROOM_AGENT_NAME;
|
|
110500
110744
|
if (selfAgent && selfAgent !== agent) {
|
|
@@ -21598,7 +21598,7 @@ function allocateAgentUid(name) {
|
|
|
21598
21598
|
}
|
|
21599
21599
|
|
|
21600
21600
|
// src/build-info.ts
|
|
21601
|
-
var VERSION = "0.21.
|
|
21601
|
+
var VERSION = "0.21.20";
|
|
21602
21602
|
|
|
21603
21603
|
// src/setup/hindsight-recall-passthrough.ts
|
|
21604
21604
|
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.21.
|
|
4
|
+
"version": "0.21.20",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -63,7 +63,12 @@ gh release create "vX.Y.Z" -R switchroom/switchroom \
|
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
- **`--draft` is mandatory.** A published release with no assets immediately becomes `/releases/latest` and 404s every `curl | sh` install for the entire ~25-minute build window. `release.yml` will forcibly re-draft an incomplete published release within about a minute, but do not rely on the safety net — it exists for the case where this step was done wrong.
|
|
66
|
-
- Creating the release object *before* the tag is
|
|
66
|
+
- **Creating the release object *before* the tag is effectively REQUIRED, not belt-and-suspenders.** `release.yml`'s `guard` job polls ~150s for the release to appear, and if it never does it auto-creates the draft from `CHANGELOG.md` (#4331). But that auto-create routinely lands **too late for the same `release` run's completeness check**: the draft is created at the tail of the poll window, the check re-reads the release list in the *same* job, still cannot see it, exits 4, and **fails the run** — `npm`/`publish`/`finalize` all `needs:` this job, so they SKIP and nothing ships. This is not hypothetical: on **v0.21.18** (`release` run `32095648269`, attempt 1) the tag was pushed with no draft, `guard` retried 10× over ~150s, logged `auto-created draft release v0.21.18 from CHANGELOG.md`, then still exited 4 (`auto-created v0.21.18 but the completeness check still cannot see it`) and the job failed at `03:33:23Z` — npm/publish/latest all SKIPPED. So do not read #4331 as "skipping this step is fine": create the draft here, on the pinned SHA, before the tag push. It also lets you author a title/notes that differ from the raw CHANGELOG section.
|
|
67
|
+
- **If the tag is already pushed without a draft and `guard` has failed, the fix-forward is a re-run, not a re-cut.** The auto-create has by then left a real draft object behind, so re-run the failed jobs — the guard's completeness check now sees the draft and passes:
|
|
68
|
+
```bash
|
|
69
|
+
gh run rerun <release-run-id> --failed -R switchroom/switchroom
|
|
70
|
+
```
|
|
71
|
+
On the second attempt `guard` passes → binaries attach → npm publishes → the GitHub Release un-drafts → `:vX.Y.Z` promotes to `:latest`. This is exactly how v0.21.18 recovered (attempt 2 published `03:37:03Z`). Do NOT cut a new patch version to route around a failed guard — the release for this tag already exists as a draft.
|
|
67
72
|
- **Notes extraction gotcha (historical):** the naive `awk '/^## vX/,/^## v/' CHANGELOG` range collapses to a single line. Use a start-flag awk: `awk 'f{print} /^## vX\.Y\.Z/{print; f=1} f && /^## v/ && !/^## vX\.Y\.Z/{exit}'` — or extract the section to a temp file by line range.
|
|
68
73
|
- **`gh release create` has been silently dropped in past runs.** Verify it exists and is a draft:
|
|
69
74
|
```bash
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* connection-drop.ts — the ONE canonical "was this a mid-stream connection /
|
|
3
|
+
* SSE drop?" wording matcher.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM this closes
|
|
6
|
+
* -----------------------
|
|
7
|
+
* Two independent classifiers historically disagreed about what a mid-stream
|
|
8
|
+
* "connection lost" / SSE-drop is, and they are consulted on different code
|
|
9
|
+
* paths:
|
|
10
|
+
*
|
|
11
|
+
* - Path A — `parseLlmError` (llm-error-present.ts) → `detectModelUnavailable`
|
|
12
|
+
* (model-unavailable.ts): a network-drop wording maps to
|
|
13
|
+
* `{kind:'transient', source:'network'}`.
|
|
14
|
+
* - Path B — the sub-agent transcript path → `detectErrorInTranscriptLine`
|
|
15
|
+
* (session-tail.ts) → `classifyClaudeError` (operator-events.ts): a
|
|
16
|
+
* connection-drop-worded line that is NOT the exact `server_error` /
|
|
17
|
+
* `api_error` type falls through to `unknown-5xx`/`unknown-4xx` (a generic
|
|
18
|
+
* terminal), because that path never consulted the network-drop wording
|
|
19
|
+
* list.
|
|
20
|
+
*
|
|
21
|
+
* So the SAME dropped-stream line was a "transient network" event on one path
|
|
22
|
+
* and a "generic terminal" event on the other. This module gives both paths a
|
|
23
|
+
* single predicate to consult so a connection-drop is identifiable as such on
|
|
24
|
+
* BOTH.
|
|
25
|
+
*
|
|
26
|
+
* SCOPE: this is a WORDING matcher only. It answers "does this text look like a
|
|
27
|
+
* transport connection drop?" — it deliberately does NOT decide the final
|
|
28
|
+
* error kind. Callers gate the raw predicate on their own classification (only
|
|
29
|
+
* flagging a drop on the transient/unknown/transport families, never on a
|
|
30
|
+
* positively-identified auth/quota/credit wall) so a wrapped auth/quota error
|
|
31
|
+
* whose OUTER text happens to say "fetch failed" is never mislabelled a drop.
|
|
32
|
+
*
|
|
33
|
+
* Pure module: no IPC, no bot, no FS. Trivially unit-testable.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Canonical connection-drop / SSE-drop wordings (lowercase substrings, matched
|
|
38
|
+
* case-insensitively). Provenance for each entry is in the PR description; the
|
|
39
|
+
* list is deliberately CONSERVATIVE — it covers transport-layer connection
|
|
40
|
+
* drops only and must NOT match auth / quota / overload / provider-credit
|
|
41
|
+
* wording (verified by the negative tests). Bare `stream` / `terminated` are
|
|
42
|
+
* intentionally EXCLUDED: `upstream`/`downstream` contain `stream`, and
|
|
43
|
+
* `terminated` appears in unrelated contexts — both would over-match and risk a
|
|
44
|
+
* false auto-resume in the later PRs that gate on this predicate.
|
|
45
|
+
*
|
|
46
|
+
* DNS-resolution failures (`enotfound`, `eai_again`, `getaddrinfo`, "request
|
|
47
|
+
* timed out") are also excluded: a name-resolution failure is a network fault
|
|
48
|
+
* but NOT a connection drop (no connection was ever established). Path A still
|
|
49
|
+
* classifies those as its broader `network` kind, unchanged — they simply do
|
|
50
|
+
* not set the connection-drop discriminator.
|
|
51
|
+
*/
|
|
52
|
+
export const CONNECTION_DROP_SIGNALS: readonly string[] = [
|
|
53
|
+
// ── Node/undici transport error codes (worded + code forms) ──────────────
|
|
54
|
+
'socket hang up', // model-unavailable.ts networkSignals; mcp-credential-failure.ts
|
|
55
|
+
'econnreset', // model-unavailable.ts networkSignals; retry-api-call.ts; ubiquitous fixtures
|
|
56
|
+
'econnrefused', // model-unavailable.ts networkSignals
|
|
57
|
+
'etimedout', // model-unavailable.ts networkSignals; retry-api-call.ts
|
|
58
|
+
'epipe', // broken pipe on a half-dead socket (tests: pending-card-expiry, boot-sweep-gate)
|
|
59
|
+
'fetch failed', // model-unavailable.ts networkSignals; retry-api-call.ts
|
|
60
|
+
'network error', // model-unavailable.ts networkSignals
|
|
61
|
+
// ── Worded connection-lost forms ─────────────────────────────────────────
|
|
62
|
+
'connection refused', // model-unavailable.ts networkSignals
|
|
63
|
+
'connection reset', // worded ECONNRESET (retry-api-call.ts comment)
|
|
64
|
+
'connection closed', // model-unavailable.ts networkSignals; canonical "Connection closed mid-response"
|
|
65
|
+
'connection lost', // plausible SSE-drop wording (precise phrase, no over-match risk)
|
|
66
|
+
// ── Mid-response / SSE-drop markers ──────────────────────────────────────
|
|
67
|
+
'mid-response', // model-unavailable.ts networkSignals; "Connection closed mid-response"
|
|
68
|
+
'premature close', // Node/undici stream "Premature close" — a response body that ended early
|
|
69
|
+
'stream disconnected', // plausible SSE-drop wording (precise two-word phrase)
|
|
70
|
+
'stream closed', // plausible SSE-drop wording (precise two-word phrase)
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* True when `text` carries a canonical connection-drop / SSE-drop wording.
|
|
75
|
+
*
|
|
76
|
+
* WORDING ONLY — see the module header. Never throws; a non-string collapses to
|
|
77
|
+
* `false`.
|
|
78
|
+
*/
|
|
79
|
+
export function isConnectionDropText(text: string): boolean {
|
|
80
|
+
if (typeof text !== 'string' || text.length === 0) return false
|
|
81
|
+
const lower = text.toLowerCase()
|
|
82
|
+
return CONNECTION_DROP_SIGNALS.some(s => lower.includes(s))
|
|
83
|
+
}
|
|
@@ -23205,6 +23205,31 @@ var OPERATOR_ACTIONABLE_KINDS = new Set([
|
|
|
23205
23205
|
"proxy-misconfig"
|
|
23206
23206
|
]);
|
|
23207
23207
|
|
|
23208
|
+
// connection-drop.ts
|
|
23209
|
+
var CONNECTION_DROP_SIGNALS = [
|
|
23210
|
+
"socket hang up",
|
|
23211
|
+
"econnreset",
|
|
23212
|
+
"econnrefused",
|
|
23213
|
+
"etimedout",
|
|
23214
|
+
"epipe",
|
|
23215
|
+
"fetch failed",
|
|
23216
|
+
"network error",
|
|
23217
|
+
"connection refused",
|
|
23218
|
+
"connection reset",
|
|
23219
|
+
"connection closed",
|
|
23220
|
+
"connection lost",
|
|
23221
|
+
"mid-response",
|
|
23222
|
+
"premature close",
|
|
23223
|
+
"stream disconnected",
|
|
23224
|
+
"stream closed"
|
|
23225
|
+
];
|
|
23226
|
+
function isConnectionDropText(text) {
|
|
23227
|
+
if (typeof text !== "string" || text.length === 0)
|
|
23228
|
+
return false;
|
|
23229
|
+
const lower = text.toLowerCase();
|
|
23230
|
+
return CONNECTION_DROP_SIGNALS.some((s) => lower.includes(s));
|
|
23231
|
+
}
|
|
23232
|
+
|
|
23208
23233
|
// tool-label-sidecar.ts
|
|
23209
23234
|
import { existsSync as existsSync2, readFileSync, statSync as statSync2 } from "node:fs";
|
|
23210
23235
|
import { join as join2 } from "node:path";
|
|
@@ -23677,6 +23702,10 @@ function extractRetryState(obj) {
|
|
|
23677
23702
|
maxRetries: typeof obj.maxRetries === "number" ? obj.maxRetries : null
|
|
23678
23703
|
};
|
|
23679
23704
|
}
|
|
23705
|
+
var CONNECTION_DROP_ELIGIBLE_KINDS = new Set(["transport-transient", "unknown-5xx", "unknown-4xx"]);
|
|
23706
|
+
function isConnectionDrop(kind, scanText) {
|
|
23707
|
+
return CONNECTION_DROP_ELIGIBLE_KINDS.has(kind) && isConnectionDropText(scanText);
|
|
23708
|
+
}
|
|
23680
23709
|
function detectErrorInTranscriptLine(line) {
|
|
23681
23710
|
if (!line || line.length > 2 * 1024 * 1024)
|
|
23682
23711
|
return null;
|
|
@@ -23701,7 +23730,9 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
23701
23730
|
raw: obj,
|
|
23702
23731
|
detail: text || errStr || "api error",
|
|
23703
23732
|
transient: kind2 === "rate-limited",
|
|
23704
|
-
terminal: true
|
|
23733
|
+
terminal: true,
|
|
23734
|
+
connectionDrop: isConnectionDrop(kind2, `${text}
|
|
23735
|
+
${errStr}`)
|
|
23705
23736
|
};
|
|
23706
23737
|
}
|
|
23707
23738
|
const isErrorLine = type === "api_error" || type === "error";
|
|
@@ -23714,7 +23745,15 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
23714
23745
|
const transient = kind === "rate-limited" || kind === "transport-transient";
|
|
23715
23746
|
const retry = extractRetryState(obj);
|
|
23716
23747
|
const terminal = !transient ? true : retry.retryAttempt != null && retry.maxRetries != null ? retry.retryAttempt >= retry.maxRetries : isErrorLine;
|
|
23717
|
-
return {
|
|
23748
|
+
return {
|
|
23749
|
+
kind,
|
|
23750
|
+
raw,
|
|
23751
|
+
detail,
|
|
23752
|
+
transient,
|
|
23753
|
+
terminal,
|
|
23754
|
+
connectionDrop: isConnectionDrop(kind, `${detail}
|
|
23755
|
+
${String(type ?? "")}`)
|
|
23756
|
+
};
|
|
23718
23757
|
}
|
|
23719
23758
|
function extractDetailMessage(obj) {
|
|
23720
23759
|
if (!obj)
|
|
@@ -74529,6 +74529,31 @@ var OPERATOR_ACTIONABLE_KINDS = new Set([
|
|
|
74529
74529
|
"proxy-misconfig"
|
|
74530
74530
|
]);
|
|
74531
74531
|
|
|
74532
|
+
// connection-drop.ts
|
|
74533
|
+
var CONNECTION_DROP_SIGNALS = [
|
|
74534
|
+
"socket hang up",
|
|
74535
|
+
"econnreset",
|
|
74536
|
+
"econnrefused",
|
|
74537
|
+
"etimedout",
|
|
74538
|
+
"epipe",
|
|
74539
|
+
"fetch failed",
|
|
74540
|
+
"network error",
|
|
74541
|
+
"connection refused",
|
|
74542
|
+
"connection reset",
|
|
74543
|
+
"connection closed",
|
|
74544
|
+
"connection lost",
|
|
74545
|
+
"mid-response",
|
|
74546
|
+
"premature close",
|
|
74547
|
+
"stream disconnected",
|
|
74548
|
+
"stream closed"
|
|
74549
|
+
];
|
|
74550
|
+
function isConnectionDropText(text4) {
|
|
74551
|
+
if (typeof text4 !== "string" || text4.length === 0)
|
|
74552
|
+
return false;
|
|
74553
|
+
const lower = text4.toLowerCase();
|
|
74554
|
+
return CONNECTION_DROP_SIGNALS.some((s) => lower.includes(s));
|
|
74555
|
+
}
|
|
74556
|
+
|
|
74532
74557
|
// session-tail.ts
|
|
74533
74558
|
function sanitizeCwdToProjectName(cwd) {
|
|
74534
74559
|
return cwd.replace(/[^a-zA-Z0-9]/g, "-");
|
|
@@ -74783,6 +74808,7 @@ function projectTranscriptLine(line) {
|
|
|
74783
74808
|
}
|
|
74784
74809
|
return [];
|
|
74785
74810
|
}
|
|
74811
|
+
var CONNECTION_DROP_ELIGIBLE_KINDS = new Set(["transport-transient", "unknown-5xx", "unknown-4xx"]);
|
|
74786
74812
|
|
|
74787
74813
|
// pty-tail.ts
|
|
74788
74814
|
var import_headless = __toESM(require_xterm_headless(), 1);
|
|
@@ -78601,6 +78627,7 @@ function parseLlmError(raw, retryState) {
|
|
|
78601
78627
|
terminal = true;
|
|
78602
78628
|
}
|
|
78603
78629
|
}
|
|
78630
|
+
const connectionDrop = (kind === "transient" || kind === "unknown") && isConnectionDropText(text4);
|
|
78604
78631
|
return {
|
|
78605
78632
|
kind,
|
|
78606
78633
|
coreText: buildCoreText(kind, source),
|
|
@@ -78610,6 +78637,7 @@ function parseLlmError(raw, retryState) {
|
|
|
78610
78637
|
...requestId != null ? { requestId } : {},
|
|
78611
78638
|
...providerId != null ? { providerId } : {},
|
|
78612
78639
|
source,
|
|
78640
|
+
connectionDrop,
|
|
78613
78641
|
autoRetrying,
|
|
78614
78642
|
terminal
|
|
78615
78643
|
};
|
|
@@ -101454,6 +101482,10 @@ function extractRetryState(obj) {
|
|
|
101454
101482
|
maxRetries: typeof obj.maxRetries === "number" ? obj.maxRetries : null
|
|
101455
101483
|
};
|
|
101456
101484
|
}
|
|
101485
|
+
var CONNECTION_DROP_ELIGIBLE_KINDS2 = new Set(["transport-transient", "unknown-5xx", "unknown-4xx"]);
|
|
101486
|
+
function isConnectionDrop(kind, scanText) {
|
|
101487
|
+
return CONNECTION_DROP_ELIGIBLE_KINDS2.has(kind) && isConnectionDropText(scanText);
|
|
101488
|
+
}
|
|
101457
101489
|
function detectErrorInTranscriptLine(line) {
|
|
101458
101490
|
if (!line || line.length > 2 * 1024 * 1024)
|
|
101459
101491
|
return null;
|
|
@@ -101478,7 +101510,9 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
101478
101510
|
raw: obj,
|
|
101479
101511
|
detail: text4 || errStr || "api error",
|
|
101480
101512
|
transient: kind2 === "rate-limited",
|
|
101481
|
-
terminal: true
|
|
101513
|
+
terminal: true,
|
|
101514
|
+
connectionDrop: isConnectionDrop(kind2, `${text4}
|
|
101515
|
+
${errStr}`)
|
|
101482
101516
|
};
|
|
101483
101517
|
}
|
|
101484
101518
|
const isErrorLine = type === "api_error" || type === "error";
|
|
@@ -101491,7 +101525,15 @@ ${errStr}`) ? "rate-limited" : "quota-exhausted" : classifyClaudeError({ type: e
|
|
|
101491
101525
|
const transient = kind === "rate-limited" || kind === "transport-transient";
|
|
101492
101526
|
const retry = extractRetryState(obj);
|
|
101493
101527
|
const terminal = !transient ? true : retry.retryAttempt != null && retry.maxRetries != null ? retry.retryAttempt >= retry.maxRetries : isErrorLine;
|
|
101494
|
-
return {
|
|
101528
|
+
return {
|
|
101529
|
+
kind,
|
|
101530
|
+
raw,
|
|
101531
|
+
detail,
|
|
101532
|
+
transient,
|
|
101533
|
+
terminal,
|
|
101534
|
+
connectionDrop: isConnectionDrop(kind, `${detail}
|
|
101535
|
+
${String(type ?? "")}`)
|
|
101536
|
+
};
|
|
101495
101537
|
}
|
|
101496
101538
|
function extractDetailMessage(obj) {
|
|
101497
101539
|
if (!obj)
|
|
@@ -105876,10 +105918,10 @@ function startOutboxSweep(deps) {
|
|
|
105876
105918
|
}
|
|
105877
105919
|
|
|
105878
105920
|
// ../src/build-info.ts
|
|
105879
|
-
var VERSION2 = "0.21.
|
|
105880
|
-
var COMMIT_SHA = "
|
|
105881
|
-
var COMMIT_DATE = "2026-08-
|
|
105882
|
-
var LATEST_PR =
|
|
105921
|
+
var VERSION2 = "0.21.20";
|
|
105922
|
+
var COMMIT_SHA = "fa05da22";
|
|
105923
|
+
var COMMIT_DATE = "2026-08-18T20:00:16Z";
|
|
105924
|
+
var LATEST_PR = 4782;
|
|
105883
105925
|
var COMMITS_AHEAD_OF_TAG = 0;
|
|
105884
105926
|
|
|
105885
105927
|
// gateway/boot-version.ts
|