switchroom 0.14.2 → 0.14.4
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 +81 -5
- package/package.json +1 -1
- package/telegram-plugin/dist/bridge/bridge.js +15 -2
- package/telegram-plugin/dist/gateway/gateway.js +97 -132
- package/telegram-plugin/dist/server.js +15 -2
- package/telegram-plugin/gateway/gateway.ts +174 -29
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +33 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +13 -4
- package/telegram-plugin/permission-rule.ts +22 -0
- package/telegram-plugin/session-tail.ts +18 -0
- package/telegram-plugin/tests/always-allow-grant.test.ts +147 -0
- package/telegram-plugin/tests/always-allow-persist.test.ts +124 -0
- package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +93 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +19 -0
- package/telegram-plugin/tests/tool-label-sidecar.test.ts +36 -0
- package/telegram-plugin/tool-activity-summary.ts +18 -0
- package/telegram-plugin/tool-label-sidecar.ts +31 -5
- package/telegram-plugin/uat/scenarios/fuzz-status-ask-dm.test.ts +39 -13
package/dist/cli/switchroom.js
CHANGED
|
@@ -30840,8 +30840,11 @@ function defaultStatBroker(p) {
|
|
|
30840
30840
|
return { kind: "ok-with-stat", ino: inoStr, size };
|
|
30841
30841
|
}
|
|
30842
30842
|
function spawnDockerStat(p) {
|
|
30843
|
+
return spawnDockerStatForContainer("switchroom-vault-broker", p);
|
|
30844
|
+
}
|
|
30845
|
+
function spawnDockerStatForContainer(containerName2, p) {
|
|
30843
30846
|
try {
|
|
30844
|
-
const stdout = execFileSync16("docker", ["exec",
|
|
30847
|
+
const stdout = execFileSync16("docker", ["exec", containerName2, "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
|
|
30845
30848
|
return { status: 0, stdout, stderr: "", error: null };
|
|
30846
30849
|
} catch (err) {
|
|
30847
30850
|
const e = err;
|
|
@@ -30933,9 +30936,80 @@ function runVaultBrokerDurabilityChecks(_config, opts) {
|
|
|
30933
30936
|
probeMachineIdMount(),
|
|
30934
30937
|
formatBindMountResult("vault-broker: vault.enc bind mount", join52(home2, ".switchroom", "vault", "vault.enc"), "/state/vault/vault.enc", probe2(join52(home2, ".switchroom", "vault", "vault.enc"), "/state/vault/vault.enc")),
|
|
30935
30938
|
formatBindMountResult("vault-broker: vault-grants.db bind mount (#1737)", join52(home2, ".switchroom", "vault-grants.db"), "/root/.switchroom/vault-grants.db", probe2(join52(home2, ".switchroom", "vault-grants.db"), "/root/.switchroom/vault-grants.db")),
|
|
30936
|
-
formatBindMountResult("vault-broker: vault-audit.log bind mount (#1025)", join52(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log", probe2(join52(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log"))
|
|
30939
|
+
formatBindMountResult("vault-broker: vault-audit.log bind mount (#1025)", join52(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log", probe2(join52(home2, ".switchroom", "vault-audit.log"), "/root/.switchroom/vault-audit.log")),
|
|
30940
|
+
probeKernelDbDurability(home2, {
|
|
30941
|
+
statBroker: opts?.kernelStatBroker
|
|
30942
|
+
})
|
|
30937
30943
|
];
|
|
30938
30944
|
}
|
|
30945
|
+
function probeKernelDbDurability(home2, opts) {
|
|
30946
|
+
const hostDir = join52(home2, ".switchroom", "approvals");
|
|
30947
|
+
const containerDir = "/state/approvals";
|
|
30948
|
+
const name = "approval-kernel: approvals bind mount (allow_always durability)";
|
|
30949
|
+
const kernelStat = opts?.statBroker ?? defaultKernelStatBroker;
|
|
30950
|
+
const result = probeBindMountInode(hostDir, containerDir, {
|
|
30951
|
+
statBroker: kernelStat,
|
|
30952
|
+
statHost: opts?.statHost
|
|
30953
|
+
});
|
|
30954
|
+
if (result.kind === "ok") {
|
|
30955
|
+
return {
|
|
30956
|
+
name,
|
|
30957
|
+
status: "ok",
|
|
30958
|
+
detail: `${hostDir} == ${containerDir} (same inode) \u2014 allow_always decisions persist across kernel recreate`
|
|
30959
|
+
};
|
|
30960
|
+
}
|
|
30961
|
+
if (result.kind === "host-missing") {
|
|
30962
|
+
return {
|
|
30963
|
+
name,
|
|
30964
|
+
status: "warn",
|
|
30965
|
+
detail: `host directory ${hostDir} missing \u2014 \`switchroom apply\` pre-creates it on greenfield`,
|
|
30966
|
+
fix: "Run `switchroom apply` to pre-create the host approvals directory"
|
|
30967
|
+
};
|
|
30968
|
+
}
|
|
30969
|
+
if (result.kind === "broker-unreachable") {
|
|
30970
|
+
return {
|
|
30971
|
+
name,
|
|
30972
|
+
status: "skip",
|
|
30973
|
+
detail: "approval-kernel container unreachable \u2014 bind mount unverified"
|
|
30974
|
+
};
|
|
30975
|
+
}
|
|
30976
|
+
if (result.kind === "broker-stat-failed") {
|
|
30977
|
+
return {
|
|
30978
|
+
name,
|
|
30979
|
+
status: "warn",
|
|
30980
|
+
detail: `approval-kernel stat failed: ${result.msg}`
|
|
30981
|
+
};
|
|
30982
|
+
}
|
|
30983
|
+
return {
|
|
30984
|
+
name,
|
|
30985
|
+
status: "fail",
|
|
30986
|
+
detail: `inode mismatch \u2014 approval-kernel \`/state/approvals\` is NOT backed by the host bind mount. ` + `host inode=${result.hostInode} size=${result.hostSize}; ` + `kernel inode=${result.brokerInode} size=${result.brokerSize}. ` + `The kernel is writing kernel.db to an ephemeral container-local directory; ` + `all allow_always decisions are lost on every container recreate (e.g. after \`switchroom update\`).`,
|
|
30987
|
+
fix: "Run `switchroom apply` to regenerate compose with the " + "`~/.switchroom/approvals:/state/approvals` bind mount, then " + "`docker compose -p switchroom up -d approval-kernel` to recreate the kernel container."
|
|
30988
|
+
};
|
|
30989
|
+
}
|
|
30990
|
+
function defaultKernelStatBroker(p) {
|
|
30991
|
+
const r = spawnDockerStatForContainer("switchroom-approval-kernel", p);
|
|
30992
|
+
if (r.error || r.status === null)
|
|
30993
|
+
return { kind: "broker-unreachable" };
|
|
30994
|
+
if (r.status !== 0) {
|
|
30995
|
+
if (r.status >= 125)
|
|
30996
|
+
return { kind: "broker-unreachable" };
|
|
30997
|
+
return {
|
|
30998
|
+
kind: "broker-stat-failed",
|
|
30999
|
+
msg: r.stderr?.trim() || `exit ${r.status}`
|
|
31000
|
+
};
|
|
31001
|
+
}
|
|
31002
|
+
const out = r.stdout.trim();
|
|
31003
|
+
const [inoStr, sizeStr] = out.split(/\s+/);
|
|
31004
|
+
const size = Number(sizeStr);
|
|
31005
|
+
if (!inoStr || !Number.isFinite(size)) {
|
|
31006
|
+
return {
|
|
31007
|
+
kind: "broker-stat-failed",
|
|
31008
|
+
msg: `unparseable stat output: ${out}`
|
|
31009
|
+
};
|
|
31010
|
+
}
|
|
31011
|
+
return { kind: "ok-with-stat", ino: inoStr, size };
|
|
31012
|
+
}
|
|
30939
31013
|
function probeAutoUnlockBlob(home2) {
|
|
30940
31014
|
const blobPath = join52(home2, ".switchroom", "vault-auto-unlock");
|
|
30941
31015
|
if (!existsSync52(blobPath)) {
|
|
@@ -49278,8 +49352,8 @@ var {
|
|
|
49278
49352
|
} = import__.default;
|
|
49279
49353
|
|
|
49280
49354
|
// src/build-info.ts
|
|
49281
|
-
var VERSION = "0.14.
|
|
49282
|
-
var COMMIT_SHA = "
|
|
49355
|
+
var VERSION = "0.14.4";
|
|
49356
|
+
var COMMIT_SHA = "a9f2d29a";
|
|
49283
49357
|
|
|
49284
49358
|
// src/cli/agent.ts
|
|
49285
49359
|
init_source();
|
|
@@ -51763,7 +51837,9 @@ function buildSettingsHooksBlock(p) {
|
|
|
51763
51837
|
` + `So:
|
|
51764
51838
|
` + " - Trivial / social message \u2192 reply once, briefly, in your voice. " + `The reply IS the response.
|
|
51765
51839
|
` + ` - Question with a short answer \u2192 just reply with the answer.
|
|
51766
|
-
` + " - Complex tool-driven work \u2192 go straight to the tools (the " + "compose-area preview is the ambient liveness signal), then reply " + 'once with the answer or a genuine mid-work pivot ("halfway ' + 'through \u2014 found an unexpected issue, want me to continue?"). Not ' +
|
|
51840
|
+
` + " - Complex tool-driven work \u2192 go straight to the tools (the " + "compose-area preview is the ambient liveness signal), then reply " + 'once with the answer or a genuine mid-work pivot ("halfway ' + 'through \u2014 found an unexpected issue, want me to continue?"). Not ' + `"still working".
|
|
51841
|
+
|
|
51842
|
+
` + 'Do NOT send a trailing confirmation after your answer \u2014 no "Done.", ' + '"Sent.", "Hope that helps." as a separate message once you have ' + "already replied. Your answer is the last thing the user should " + `see; a follow-up "Done." is dead-air clutter (and the user's ` + "device already pinged on the answer). Stop after the answer.</turn-pacing>";
|
|
51767
51843
|
const switchroomUserPromptSubmit = [
|
|
51768
51844
|
...useHotReloadStable ? [
|
|
51769
51845
|
{
|
package/package.json
CHANGED
|
@@ -23041,6 +23041,7 @@ import { join as join2 } from "node:path";
|
|
|
23041
23041
|
function createToolLabelSidecar(opts) {
|
|
23042
23042
|
const path = join2(opts.stateDir, `tool-labels-${opts.sessionId}.jsonl`);
|
|
23043
23043
|
const labels = new Map;
|
|
23044
|
+
const seen = [];
|
|
23044
23045
|
const subscribers = new Set;
|
|
23045
23046
|
let offset = 0;
|
|
23046
23047
|
let stopped = false;
|
|
@@ -23063,14 +23064,15 @@ function createToolLabelSidecar(opts) {
|
|
|
23063
23064
|
} catch {
|
|
23064
23065
|
continue;
|
|
23065
23066
|
}
|
|
23066
|
-
if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string")
|
|
23067
|
+
if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string" || typeof row.tool_name !== "string")
|
|
23067
23068
|
continue;
|
|
23068
23069
|
if (labels.has(row.tool_use_id))
|
|
23069
23070
|
continue;
|
|
23070
23071
|
labels.set(row.tool_use_id, row.label);
|
|
23072
|
+
seen.push({ toolUseId: row.tool_use_id, label: row.label, toolName: row.tool_name });
|
|
23071
23073
|
for (const cb of subscribers) {
|
|
23072
23074
|
try {
|
|
23073
|
-
cb(row.tool_use_id, row.label);
|
|
23075
|
+
cb(row.tool_use_id, row.label, row.tool_name);
|
|
23074
23076
|
} catch {}
|
|
23075
23077
|
}
|
|
23076
23078
|
}
|
|
@@ -23109,6 +23111,11 @@ function createToolLabelSidecar(opts) {
|
|
|
23109
23111
|
return labels.get(toolUseId);
|
|
23110
23112
|
},
|
|
23111
23113
|
onLabel(cb) {
|
|
23114
|
+
for (const r of seen) {
|
|
23115
|
+
try {
|
|
23116
|
+
cb(r.toolUseId, r.label, r.toolName);
|
|
23117
|
+
} catch {}
|
|
23118
|
+
}
|
|
23112
23119
|
subscribers.add(cb);
|
|
23113
23120
|
return () => subscribers.delete(cb);
|
|
23114
23121
|
},
|
|
@@ -23441,6 +23448,9 @@ function startSessionTail(config2) {
|
|
|
23441
23448
|
try {
|
|
23442
23449
|
const s = createToolLabelSidecar({ stateDir: stateDirForSidecar, sessionId });
|
|
23443
23450
|
sidecars.set(sessionId, s);
|
|
23451
|
+
s.onLabel((toolUseId, label, toolName) => {
|
|
23452
|
+
rawOnEvent({ kind: "tool_label", toolUseId, label, toolName });
|
|
23453
|
+
});
|
|
23444
23454
|
return s;
|
|
23445
23455
|
} catch (err) {
|
|
23446
23456
|
log?.(`session-tail: sidecar create failed: ${err.message}`);
|
|
@@ -23554,6 +23564,9 @@ function startSessionTail(config2) {
|
|
|
23554
23564
|
}
|
|
23555
23565
|
log?.(`session-tail: attached to ${file} (cursor=${cursor})`);
|
|
23556
23566
|
}
|
|
23567
|
+
const attachSid = sessionIdForFile(file);
|
|
23568
|
+
if (attachSid)
|
|
23569
|
+
ensureSidecar(attachSid);
|
|
23557
23570
|
try {
|
|
23558
23571
|
watcher = watch(file, () => readNew());
|
|
23559
23572
|
} catch (err) {
|
|
@@ -31866,121 +31866,7 @@ function registerAndRender(state, toolName) {
|
|
|
31866
31866
|
return null;
|
|
31867
31867
|
return formatSummary(state);
|
|
31868
31868
|
}
|
|
31869
|
-
function baseName(p) {
|
|
31870
|
-
if (typeof p !== "string" || p.length === 0)
|
|
31871
|
-
return null;
|
|
31872
|
-
const parts = p.split("/").filter(Boolean);
|
|
31873
|
-
return parts.length > 0 ? parts[parts.length - 1] : p;
|
|
31874
|
-
}
|
|
31875
|
-
function hostName(u) {
|
|
31876
|
-
if (typeof u !== "string" || u.length === 0)
|
|
31877
|
-
return null;
|
|
31878
|
-
try {
|
|
31879
|
-
return new URL(u).hostname.replace(/^www\./, "");
|
|
31880
|
-
} catch {
|
|
31881
|
-
return u.replace(/^https?:\/\//, "").split("/")[0] || null;
|
|
31882
|
-
}
|
|
31883
|
-
}
|
|
31884
|
-
function clip(s, n) {
|
|
31885
|
-
if (typeof s !== "string")
|
|
31886
|
-
return null;
|
|
31887
|
-
const t = s.trim();
|
|
31888
|
-
if (t.length === 0)
|
|
31889
|
-
return null;
|
|
31890
|
-
return t.length > n ? t.slice(0, n - 1) + "\u2026" : t;
|
|
31891
|
-
}
|
|
31892
|
-
function describeToolUse(toolName, input) {
|
|
31893
|
-
if (!toolName)
|
|
31894
|
-
return null;
|
|
31895
|
-
const inp = input ?? {};
|
|
31896
|
-
const mcpMatch = /^mcp__(.+?)__(.+)$/.exec(toolName);
|
|
31897
|
-
if (mcpMatch) {
|
|
31898
|
-
const server = mcpMatch[1].toLowerCase();
|
|
31899
|
-
const tool = mcpMatch[2].toLowerCase();
|
|
31900
|
-
if (server === "switchroom-telegram")
|
|
31901
|
-
return null;
|
|
31902
|
-
if (server === "hindsight") {
|
|
31903
|
-
if (tool === "recall" || tool === "reflect")
|
|
31904
|
-
return "Searching memory";
|
|
31905
|
-
if (tool === "retain" || tool === "update_memory" || tool === "sync_retain")
|
|
31906
|
-
return "Saving to memory";
|
|
31907
|
-
return "Working with memory";
|
|
31908
|
-
}
|
|
31909
|
-
if (server === "google-workspace" || server === "claude_ai_google_calendar") {
|
|
31910
|
-
return "Checking your calendar";
|
|
31911
|
-
}
|
|
31912
|
-
if (server === "claude_ai_gmail")
|
|
31913
|
-
return "Checking your email";
|
|
31914
|
-
if (server === "claude_ai_google_drive")
|
|
31915
|
-
return "Looking through your files";
|
|
31916
|
-
if (server === "notion" || server === "claude_ai_notion") {
|
|
31917
|
-
return "Checking your notes";
|
|
31918
|
-
}
|
|
31919
|
-
const desc = clip(inp.description, 60) ?? clip(inp.query, 50) ?? clip(inp.title, 50);
|
|
31920
|
-
if (desc)
|
|
31921
|
-
return desc;
|
|
31922
|
-
return "Using " + tool.replace(/[-_]+/g, " ");
|
|
31923
|
-
}
|
|
31924
|
-
switch (toolName) {
|
|
31925
|
-
case "Bash": {
|
|
31926
|
-
return clip(inp.description, 70) ?? "Running a command";
|
|
31927
|
-
}
|
|
31928
|
-
case "BashOutput":
|
|
31929
|
-
case "KillShell":
|
|
31930
|
-
return "Managing a background command";
|
|
31931
|
-
case "Read": {
|
|
31932
|
-
const f = baseName(inp.file_path);
|
|
31933
|
-
return f ? `Reading ${f}` : "Reading a file";
|
|
31934
|
-
}
|
|
31935
|
-
case "Edit":
|
|
31936
|
-
case "MultiEdit":
|
|
31937
|
-
case "NotebookEdit": {
|
|
31938
|
-
const f = baseName(inp.file_path) ?? baseName(inp.notebook_path);
|
|
31939
|
-
return f ? `Editing ${f}` : "Editing a file";
|
|
31940
|
-
}
|
|
31941
|
-
case "Write": {
|
|
31942
|
-
const f = baseName(inp.file_path);
|
|
31943
|
-
return f ? `Writing ${f}` : "Writing a file";
|
|
31944
|
-
}
|
|
31945
|
-
case "Grep":
|
|
31946
|
-
case "Glob": {
|
|
31947
|
-
const p = clip(inp.pattern, 40);
|
|
31948
|
-
return p ? `Searching for ${p}` : "Searching files";
|
|
31949
|
-
}
|
|
31950
|
-
case "WebFetch": {
|
|
31951
|
-
const h = hostName(inp.url);
|
|
31952
|
-
return h ? `Reading ${h}` : "Reading a web page";
|
|
31953
|
-
}
|
|
31954
|
-
case "WebSearch": {
|
|
31955
|
-
const q = clip(inp.query, 50);
|
|
31956
|
-
return q ? `Searching the web for ${q}` : "Searching the web";
|
|
31957
|
-
}
|
|
31958
|
-
case "Task":
|
|
31959
|
-
case "Agent": {
|
|
31960
|
-
const d = clip(inp.description, 60);
|
|
31961
|
-
return d ? `Delegating: ${d}` : "Delegating to a sub-agent";
|
|
31962
|
-
}
|
|
31963
|
-
case "TodoWrite":
|
|
31964
|
-
case "TaskCreate":
|
|
31965
|
-
case "TaskUpdate":
|
|
31966
|
-
case "TaskList":
|
|
31967
|
-
return "Updating the plan";
|
|
31968
|
-
case "ToolSearch":
|
|
31969
|
-
return "Finding the right tool";
|
|
31970
|
-
default:
|
|
31971
|
-
return "Working\u2026";
|
|
31972
|
-
}
|
|
31973
|
-
}
|
|
31974
31869
|
var MIRROR_MAX_LINES = 6;
|
|
31975
|
-
function appendActivityLine(lines, toolName, input) {
|
|
31976
|
-
const line = describeToolUse(toolName, input);
|
|
31977
|
-
if (line == null)
|
|
31978
|
-
return null;
|
|
31979
|
-
if (lines.length === 0 || lines[lines.length - 1] !== line) {
|
|
31980
|
-
lines.push(line);
|
|
31981
|
-
}
|
|
31982
|
-
return renderActivityFeed(lines);
|
|
31983
|
-
}
|
|
31984
31870
|
function renderActivityFeed(lines) {
|
|
31985
31871
|
if (lines.length === 0)
|
|
31986
31872
|
return null;
|
|
@@ -31991,6 +31877,15 @@ function renderActivityFeed(lines) {
|
|
|
31991
31877
|
return hidden > 0 ? `\u00b7 +${hidden} earlier\u2026
|
|
31992
31878
|
${body}` : body;
|
|
31993
31879
|
}
|
|
31880
|
+
function appendActivityLabel(lines, label) {
|
|
31881
|
+
const l = (label ?? "").trim();
|
|
31882
|
+
if (l.length === 0)
|
|
31883
|
+
return null;
|
|
31884
|
+
if (lines.length === 0 || lines[lines.length - 1] !== l) {
|
|
31885
|
+
lines.push(l);
|
|
31886
|
+
}
|
|
31887
|
+
return renderActivityFeed(lines);
|
|
31888
|
+
}
|
|
31994
31889
|
|
|
31995
31890
|
// tool-labels.ts
|
|
31996
31891
|
var MAX_LABEL_CHARS = 60;
|
|
@@ -46282,6 +46177,13 @@ function transition(state3, event) {
|
|
|
46282
46177
|
// gateway/inbound-delivery-machine-shadow.ts
|
|
46283
46178
|
var state3 = initialState();
|
|
46284
46179
|
var enabled5 = process.env.SWITCHROOM_DELIVERY_MACHINE_SHADOW !== "0";
|
|
46180
|
+
var cutoverEnabled = enabled5 && process.env.SWITCHROOM_DELIVERY_MACHINE_CUTOVER !== "0";
|
|
46181
|
+
function isDeliveryCutoverEnabled() {
|
|
46182
|
+
return cutoverEnabled;
|
|
46183
|
+
}
|
|
46184
|
+
function isMachineInTurn() {
|
|
46185
|
+
return state3.global.kind === "bridge_alive_in_turn";
|
|
46186
|
+
}
|
|
46285
46187
|
function shadowEmit(event) {
|
|
46286
46188
|
if (!enabled5)
|
|
46287
46189
|
return [];
|
|
@@ -49843,6 +49745,9 @@ function skillBasenameFromPath2(input) {
|
|
|
49843
49745
|
const trimmed = path.replace(/\/SKILL\.md$/i, "").replace(/\/$/, "");
|
|
49844
49746
|
return basename6(trimmed) || null;
|
|
49845
49747
|
}
|
|
49748
|
+
function isRulePersisted(resolvedAllow, ruleRule) {
|
|
49749
|
+
return resolvedAllow.includes(ruleRule);
|
|
49750
|
+
}
|
|
49846
49751
|
|
|
49847
49752
|
// credits-watch.ts
|
|
49848
49753
|
import { readFileSync as readFileSync29, writeFileSync as writeFileSync18, existsSync as existsSync30, mkdirSync as mkdirSync16 } from "fs";
|
|
@@ -50163,11 +50068,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
50163
50068
|
}
|
|
50164
50069
|
|
|
50165
50070
|
// ../src/build-info.ts
|
|
50166
|
-
var VERSION = "0.14.
|
|
50167
|
-
var COMMIT_SHA = "
|
|
50168
|
-
var COMMIT_DATE = "2026-05-
|
|
50169
|
-
var LATEST_PR =
|
|
50170
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
50071
|
+
var VERSION = "0.14.4";
|
|
50072
|
+
var COMMIT_SHA = "a9f2d29a";
|
|
50073
|
+
var COMMIT_DATE = "2026-05-28T20:55:22+10:00";
|
|
50074
|
+
var LATEST_PR = null;
|
|
50075
|
+
var COMMITS_AHEAD_OF_TAG = 5;
|
|
50171
50076
|
|
|
50172
50077
|
// gateway/boot-version.ts
|
|
50173
50078
|
function formatRelativeAgo(iso) {
|
|
@@ -51091,6 +50996,9 @@ function markClaudeBusyForInbound(m) {
|
|
|
51091
50996
|
}
|
|
51092
50997
|
claudeBusyKeys.add(chatKey2(m.chatId, tid));
|
|
51093
50998
|
}
|
|
50999
|
+
function turnInFlightForGate() {
|
|
51000
|
+
return isDeliveryCutoverEnabled() ? isMachineInTurn() : claudeBusyKeys.size > 0;
|
|
51001
|
+
}
|
|
51094
51002
|
var pendingRestarts = new Map;
|
|
51095
51003
|
var lastSessionActiveFile = null;
|
|
51096
51004
|
var compactState = initialCompactState();
|
|
@@ -51156,7 +51064,7 @@ function purgeReactionTracking(key, endingTurn) {
|
|
|
51156
51064
|
if (agentDir != null)
|
|
51157
51065
|
removeActiveReaction(agentDir, msgInfo.chatId, msgInfo.messageId);
|
|
51158
51066
|
}
|
|
51159
|
-
if (
|
|
51067
|
+
if (!turnInFlightForGate()) {
|
|
51160
51068
|
const selfAgentForFlush = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
51161
51069
|
if (pendingInboundBuffer.depth(selfAgentForFlush) > 0) {
|
|
51162
51070
|
const fr = redeliverBufferedInbound(pendingInboundBuffer, selfAgentForFlush, (m) => {
|
|
@@ -51186,7 +51094,7 @@ function releaseTurnBufferGate(key) {
|
|
|
51186
51094
|
activeTurnStartedAt.delete(key);
|
|
51187
51095
|
claudeBusyKeys.delete(key);
|
|
51188
51096
|
shadowEmit({ kind: "turnEnd", key, at: Date.now(), outboundEmitted: true });
|
|
51189
|
-
if (
|
|
51097
|
+
if (!turnInFlightForGate()) {
|
|
51190
51098
|
const selfAgentForFlush = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
51191
51099
|
if (pendingInboundBuffer.depth(selfAgentForFlush) > 0) {
|
|
51192
51100
|
const fr = redeliverBufferedInbound(pendingInboundBuffer, selfAgentForFlush, (m) => {
|
|
@@ -52134,6 +52042,11 @@ startTimer({
|
|
|
52134
52042
|
`);
|
|
52135
52043
|
}
|
|
52136
52044
|
});
|
|
52045
|
+
var DELIVERY_MACHINE_TICK_MS = 30000;
|
|
52046
|
+
var _deliveryMachineTick = setInterval(() => {
|
|
52047
|
+
shadowEmit({ kind: "tick", now: Date.now() });
|
|
52048
|
+
}, DELIVERY_MACHINE_TICK_MS);
|
|
52049
|
+
_deliveryMachineTick.unref?.();
|
|
52137
52050
|
startTimer2({
|
|
52138
52051
|
editMessage: async (ctx) => {
|
|
52139
52052
|
const editOpts = ctx.parseMode != null ? { parse_mode: ctx.parseMode } : undefined;
|
|
@@ -52435,7 +52348,7 @@ ${reminder}
|
|
|
52435
52348
|
onHeartbeat(_client, _msg) {},
|
|
52436
52349
|
onScheduleRestart(client3, msg) {
|
|
52437
52350
|
const { agentName: agentName3 } = msg;
|
|
52438
|
-
const turnInFlight =
|
|
52351
|
+
const turnInFlight = turnInFlightForGate();
|
|
52439
52352
|
if (!turnInFlight) {
|
|
52440
52353
|
try {
|
|
52441
52354
|
client3.send({
|
|
@@ -52690,7 +52603,7 @@ if (!STATIC) {
|
|
|
52690
52603
|
setInterval(() => {
|
|
52691
52604
|
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
|
|
52692
52605
|
const r = idleDrainTick(pendingInboundBuffer, selfAgent, () => {
|
|
52693
|
-
if (
|
|
52606
|
+
if (turnInFlightForGate())
|
|
52694
52607
|
return false;
|
|
52695
52608
|
const c = ipcServer.getClient(selfAgent);
|
|
52696
52609
|
return c != null && c.isAlive();
|
|
@@ -52971,6 +52884,7 @@ ${url}`;
|
|
|
52971
52884
|
});
|
|
52972
52885
|
noteOutbound(statusKey(chat_id, threadId), Date.now());
|
|
52973
52886
|
noteOutbound2(statusKey(chat_id, threadId), Date.now());
|
|
52887
|
+
shadowEmit({ kind: "modelOutbound", key: statusKey(chat_id, threadId), at: Date.now() });
|
|
52974
52888
|
if (isFinalAnswerReply({ text: rawText, disableNotification })) {
|
|
52975
52889
|
clearSilentEndState(statusKey(chat_id, threadId));
|
|
52976
52890
|
}
|
|
@@ -53298,6 +53212,7 @@ async function executeStreamReply(args) {
|
|
|
53298
53212
|
const sKey = statusKey(streamChatId, streamThreadId);
|
|
53299
53213
|
noteOutbound(sKey, Date.now());
|
|
53300
53214
|
noteOutbound2(sKey, Date.now());
|
|
53215
|
+
shadowEmit({ kind: "modelOutbound", key: sKey, at: Date.now() });
|
|
53301
53216
|
if (isFinalAnswerReply({
|
|
53302
53217
|
text: args.text ?? "",
|
|
53303
53218
|
disableNotification: args.disable_notification === true,
|
|
@@ -54248,6 +54163,11 @@ function handleSessionEvent(ev) {
|
|
|
54248
54163
|
isDm: isDmChatId(ev.chatId)
|
|
54249
54164
|
};
|
|
54250
54165
|
currentTurn = next;
|
|
54166
|
+
shadowEmit({
|
|
54167
|
+
kind: "turnStart",
|
|
54168
|
+
key: statusKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : undefined),
|
|
54169
|
+
at: startedAt
|
|
54170
|
+
});
|
|
54251
54171
|
preambleSuppressor.reset();
|
|
54252
54172
|
clearSilentEndState(statusKey(ev.chatId, ev.threadId != null ? Number(ev.threadId) : null));
|
|
54253
54173
|
if (turnsDb != null) {
|
|
@@ -54309,12 +54229,12 @@ function handleSessionEvent(ev) {
|
|
|
54309
54229
|
clearTimeout(turn.orphanedReplyTimeoutId);
|
|
54310
54230
|
turn.orphanedReplyTimeoutId = null;
|
|
54311
54231
|
}
|
|
54312
|
-
if (wasFirstReply) {
|
|
54232
|
+
if (wasFirstReply && !DRAFT_MIRROR_ENABLED) {
|
|
54313
54233
|
clearActivitySummary(turn);
|
|
54314
54234
|
}
|
|
54315
54235
|
}
|
|
54316
|
-
if (!turn.replyCalled && !isTelegramSurfaceTool(name)) {
|
|
54317
|
-
const rendered =
|
|
54236
|
+
if (!DRAFT_MIRROR_ENABLED && !turn.replyCalled && !isTelegramSurfaceTool(name)) {
|
|
54237
|
+
const rendered = registerAndRender(turn.toolActivity, name);
|
|
54318
54238
|
if (rendered != null) {
|
|
54319
54239
|
turn.activityPendingRender = rendered;
|
|
54320
54240
|
if (turn.activityInFlight == null) {
|
|
@@ -54332,6 +54252,23 @@ function handleSessionEvent(ev) {
|
|
|
54332
54252
|
}
|
|
54333
54253
|
return;
|
|
54334
54254
|
}
|
|
54255
|
+
case "tool_label": {
|
|
54256
|
+
if (!DRAFT_MIRROR_ENABLED)
|
|
54257
|
+
return;
|
|
54258
|
+
const turn = currentTurn;
|
|
54259
|
+
if (turn == null)
|
|
54260
|
+
return;
|
|
54261
|
+
if (isTelegramSurfaceTool(ev.toolName))
|
|
54262
|
+
return;
|
|
54263
|
+
const rendered = appendActivityLabel(turn.mirrorLines, ev.label);
|
|
54264
|
+
if (rendered != null) {
|
|
54265
|
+
turn.activityPendingRender = rendered;
|
|
54266
|
+
if (turn.activityInFlight == null) {
|
|
54267
|
+
turn.activityInFlight = drainActivitySummary(turn);
|
|
54268
|
+
}
|
|
54269
|
+
}
|
|
54270
|
+
return;
|
|
54271
|
+
}
|
|
54335
54272
|
case "text": {
|
|
54336
54273
|
const turn = currentTurn;
|
|
54337
54274
|
if (turn != null) {
|
|
@@ -54461,6 +54398,9 @@ function handleSessionEvent(ev) {
|
|
|
54461
54398
|
clearTimeout(turn.orphanedReplyTimeoutId);
|
|
54462
54399
|
turn.orphanedReplyTimeoutId = null;
|
|
54463
54400
|
}
|
|
54401
|
+
if (DRAFT_MIRROR_ENABLED && turn != null) {
|
|
54402
|
+
clearActivitySummary(turn);
|
|
54403
|
+
}
|
|
54464
54404
|
preambleSuppressor.flushNow();
|
|
54465
54405
|
let streamFinalizedAsAnswer = false;
|
|
54466
54406
|
if (turn?.answerStream != null) {
|
|
@@ -55001,6 +54941,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
|
55001
54941
|
}
|
|
55002
54942
|
const inboundReceivedAt = Date.now();
|
|
55003
54943
|
const _shadowKey = statusKey(ctx.chat?.id != null ? String(ctx.chat.id) : "0", ctx.message?.message_thread_id);
|
|
54944
|
+
const machineInTurnAtReceipt = isDeliveryCutoverEnabled() ? isMachineInTurn() : null;
|
|
55004
54945
|
shadowEmit({
|
|
55005
54946
|
kind: "inbound",
|
|
55006
54947
|
key: _shadowKey,
|
|
@@ -55011,7 +54952,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
|
55011
54952
|
},
|
|
55012
54953
|
at: Date.now()
|
|
55013
54954
|
});
|
|
55014
|
-
const turnInFlightAtReceipt = claudeBusyKeys.size > 0;
|
|
54955
|
+
const turnInFlightAtReceipt = machineInTurnAtReceipt ?? claudeBusyKeys.size > 0;
|
|
55015
54956
|
const access = result.access;
|
|
55016
54957
|
const from = ctx.from;
|
|
55017
54958
|
const chat_id = String(ctx.chat.id);
|
|
@@ -59096,20 +59037,44 @@ ${prettyInput}`;
|
|
|
59096
59037
|
return;
|
|
59097
59038
|
}
|
|
59098
59039
|
let grantOk = false;
|
|
59040
|
+
let grantFailReason = "";
|
|
59099
59041
|
try {
|
|
59100
59042
|
switchroomExec(["agent", "grant", agentName3, rule.rule, "--no-restart"]);
|
|
59101
|
-
|
|
59102
|
-
|
|
59043
|
+
try {
|
|
59044
|
+
const cfg = loadConfig2();
|
|
59045
|
+
const rawAgent = cfg.agents?.[agentName3];
|
|
59046
|
+
if (rawAgent) {
|
|
59047
|
+
const resolved = resolveAgentConfig2(cfg.defaults, cfg.profiles, rawAgent);
|
|
59048
|
+
const allowList = resolved.tools?.allow ?? [];
|
|
59049
|
+
if (isRulePersisted(allowList, rule.rule)) {
|
|
59050
|
+
grantOk = true;
|
|
59051
|
+
process.stderr.write(`telegram gateway: always-allow added rule="${rule.rule}" agent=${agentName3} (request_id=${request_id})
|
|
59052
|
+
`);
|
|
59053
|
+
} else {
|
|
59054
|
+
grantFailReason = `rule "${rule.rule}" not found in resolved tools.allow after write \u2014 config location may have drifted`;
|
|
59055
|
+
process.stderr.write(`telegram gateway: always-allow VERIFY FAILED: ${grantFailReason} (request_id=${request_id})
|
|
59056
|
+
`);
|
|
59057
|
+
}
|
|
59058
|
+
} else {
|
|
59059
|
+
grantFailReason = `agent "${agentName3}" not found in config after write`;
|
|
59060
|
+
process.stderr.write(`telegram gateway: always-allow VERIFY FAILED: ${grantFailReason} (request_id=${request_id})
|
|
59103
59061
|
`);
|
|
59062
|
+
}
|
|
59063
|
+
} catch (verifyErr) {
|
|
59064
|
+
grantFailReason = `config re-read failed: ${verifyErr.message}`;
|
|
59065
|
+
process.stderr.write(`telegram gateway: always-allow VERIFY FAILED: ${grantFailReason} (request_id=${request_id})
|
|
59066
|
+
`);
|
|
59067
|
+
}
|
|
59104
59068
|
} catch (err) {
|
|
59105
|
-
|
|
59069
|
+
grantFailReason = err.message;
|
|
59070
|
+
process.stderr.write(`telegram gateway: always-allow grant failed: ${grantFailReason}
|
|
59106
59071
|
`);
|
|
59107
59072
|
}
|
|
59108
59073
|
pendingPermissions.delete(request_id);
|
|
59109
|
-
const ackText = grantOk ? `\uD83D\uDD01 Always allow ${rule.label} for ${agentName3}` : `\
|
|
59074
|
+
const ackText = grantOk ? `\uD83D\uDD01 Always allow ${rule.label} for ${agentName3}` : `\u26A0\uFE0F Allowed for now, but "always" did NOT save \u2014 it will ask again after restart. Check gateway log.`;
|
|
59110
59075
|
const sourceMsg = ctx.callbackQuery?.message;
|
|
59111
59076
|
const baseText2 = sourceMsg && "text" in sourceMsg && sourceMsg.text ? escapeHtmlForTg(sourceMsg.text) : "";
|
|
59112
|
-
const editLabel = grantOk ? `\uD83D\uDD01 <b>Always allow ${escapeHtmlForTg(rule.label)}</b> for ${escapeHtmlForTg(agentName3)} \u2014 restart agent for full effect` : `\
|
|
59077
|
+
const editLabel = grantOk ? `\uD83D\uDD01 <b>Always allow ${escapeHtmlForTg(rule.label)}</b> for ${escapeHtmlForTg(agentName3)} \u2014 restart agent for full effect` : `\u26A0\uFE0F <b>Allowed for now \u2014 "always" did NOT save.</b> It will ask again after restart. Check gateway log.`;
|
|
59113
59078
|
await finalizeCallback(ctx, {
|
|
59114
59079
|
ackText: ackText.slice(0, 200),
|
|
59115
59080
|
newText: baseText2 ? `${baseText2}
|
|
@@ -17069,6 +17069,7 @@ import { join as join3 } from "node:path";
|
|
|
17069
17069
|
function createToolLabelSidecar(opts) {
|
|
17070
17070
|
const path = join3(opts.stateDir, `tool-labels-${opts.sessionId}.jsonl`);
|
|
17071
17071
|
const labels = new Map;
|
|
17072
|
+
const seen = [];
|
|
17072
17073
|
const subscribers = new Set;
|
|
17073
17074
|
let offset = 0;
|
|
17074
17075
|
let stopped = false;
|
|
@@ -17091,14 +17092,15 @@ function createToolLabelSidecar(opts) {
|
|
|
17091
17092
|
} catch {
|
|
17092
17093
|
continue;
|
|
17093
17094
|
}
|
|
17094
|
-
if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string")
|
|
17095
|
+
if (!row || typeof row.tool_use_id !== "string" || typeof row.label !== "string" || typeof row.tool_name !== "string")
|
|
17095
17096
|
continue;
|
|
17096
17097
|
if (labels.has(row.tool_use_id))
|
|
17097
17098
|
continue;
|
|
17098
17099
|
labels.set(row.tool_use_id, row.label);
|
|
17100
|
+
seen.push({ toolUseId: row.tool_use_id, label: row.label, toolName: row.tool_name });
|
|
17099
17101
|
for (const cb of subscribers) {
|
|
17100
17102
|
try {
|
|
17101
|
-
cb(row.tool_use_id, row.label);
|
|
17103
|
+
cb(row.tool_use_id, row.label, row.tool_name);
|
|
17102
17104
|
} catch {}
|
|
17103
17105
|
}
|
|
17104
17106
|
}
|
|
@@ -17137,6 +17139,11 @@ function createToolLabelSidecar(opts) {
|
|
|
17137
17139
|
return labels.get(toolUseId);
|
|
17138
17140
|
},
|
|
17139
17141
|
onLabel(cb) {
|
|
17142
|
+
for (const r of seen) {
|
|
17143
|
+
try {
|
|
17144
|
+
cb(r.toolUseId, r.label, r.toolName);
|
|
17145
|
+
} catch {}
|
|
17146
|
+
}
|
|
17140
17147
|
subscribers.add(cb);
|
|
17141
17148
|
return () => subscribers.delete(cb);
|
|
17142
17149
|
},
|
|
@@ -17479,6 +17486,9 @@ function startSessionTail(config2) {
|
|
|
17479
17486
|
try {
|
|
17480
17487
|
const s = createToolLabelSidecar({ stateDir: stateDirForSidecar, sessionId });
|
|
17481
17488
|
sidecars.set(sessionId, s);
|
|
17489
|
+
s.onLabel((toolUseId, label, toolName) => {
|
|
17490
|
+
rawOnEvent({ kind: "tool_label", toolUseId, label, toolName });
|
|
17491
|
+
});
|
|
17482
17492
|
return s;
|
|
17483
17493
|
} catch (err) {
|
|
17484
17494
|
log?.(`session-tail: sidecar create failed: ${err.message}`);
|
|
@@ -17592,6 +17602,9 @@ function startSessionTail(config2) {
|
|
|
17592
17602
|
}
|
|
17593
17603
|
log?.(`session-tail: attached to ${file} (cursor=${cursor})`);
|
|
17594
17604
|
}
|
|
17605
|
+
const attachSid = sessionIdForFile(file);
|
|
17606
|
+
if (attachSid)
|
|
17607
|
+
ensureSidecar(attachSid);
|
|
17595
17608
|
try {
|
|
17596
17609
|
watcher = watch(file, () => readNew());
|
|
17597
17610
|
} catch (err) {
|