switchroom 0.15.20 → 0.15.22
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 +2 -2
- package/dist/host-control/main.js +88 -2
- package/package.json +1 -1
- package/telegram-plugin/dist/gateway/gateway.js +163 -88
- package/telegram-plugin/gateway/config-approval-handler.test.ts +24 -0
- package/telegram-plugin/gateway/config-approval-handler.ts +28 -1
- package/telegram-plugin/gateway/effort-command.ts +56 -47
- package/telegram-plugin/gateway/gateway.ts +60 -37
- package/telegram-plugin/gateway/ipc-protocol.ts +8 -0
- package/telegram-plugin/gateway/ipc-server.ts +10 -0
- package/telegram-plugin/tests/effort-command.test.ts +43 -34
- package/telegram-plugin/tests/ipc-validator.test.ts +28 -0
- package/telegram-plugin/tests/vault-grant-auto-resume.test.ts +15 -4
- package/telegram-plugin/tests/vault-resume-turn-gated.test.ts +97 -0
- package/telegram-plugin/uat/scenarios/jtbd-effort-command-dm.test.ts +90 -0
- package/telegram-plugin/uat/scenarios/jtbd-grant-resume-telegram-id-dm.test.ts +97 -0
- package/telegram-plugin/uat/scenarios/jtbd-model-command-dm.test.ts +7 -4
- package/telegram-plugin/uat/scenarios/jtbd-model-tap-dm.test.ts +71 -0
- package/telegram-plugin/uat/scenarios/jtbd-whoami-dm.test.ts +40 -0
package/dist/cli/switchroom.js
CHANGED
|
@@ -50477,8 +50477,8 @@ var {
|
|
|
50477
50477
|
} = import__.default;
|
|
50478
50478
|
|
|
50479
50479
|
// src/build-info.ts
|
|
50480
|
-
var VERSION = "0.15.
|
|
50481
|
-
var COMMIT_SHA = "
|
|
50480
|
+
var VERSION = "0.15.22";
|
|
50481
|
+
var COMMIT_SHA = "a6c13429";
|
|
50482
50482
|
|
|
50483
50483
|
// src/cli/agent.ts
|
|
50484
50484
|
init_source();
|
|
@@ -20817,6 +20817,85 @@ function stripCallerAllow(cfg, caller) {
|
|
|
20817
20817
|
return clone;
|
|
20818
20818
|
}
|
|
20819
20819
|
|
|
20820
|
+
// src/host-control/config-blast-radius.ts
|
|
20821
|
+
function toObject2(v) {
|
|
20822
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
20823
|
+
}
|
|
20824
|
+
function changedConfigPaths(before, after, prefix = "") {
|
|
20825
|
+
if (deepEqual(before, after))
|
|
20826
|
+
return [];
|
|
20827
|
+
const bObj = before && typeof before === "object" && !Array.isArray(before);
|
|
20828
|
+
const aObj = after && typeof after === "object" && !Array.isArray(after);
|
|
20829
|
+
if (bObj && aObj) {
|
|
20830
|
+
const keys = new Set([
|
|
20831
|
+
...Object.keys(before),
|
|
20832
|
+
...Object.keys(after)
|
|
20833
|
+
]);
|
|
20834
|
+
const out = [];
|
|
20835
|
+
for (const k of keys) {
|
|
20836
|
+
out.push(...changedConfigPaths(before[k], after[k], prefix ? `${prefix}.${k}` : k));
|
|
20837
|
+
}
|
|
20838
|
+
return out;
|
|
20839
|
+
}
|
|
20840
|
+
return [prefix || "<root>"];
|
|
20841
|
+
}
|
|
20842
|
+
function deepEqual(a, b) {
|
|
20843
|
+
if (a === b)
|
|
20844
|
+
return true;
|
|
20845
|
+
if (typeof a !== typeof b)
|
|
20846
|
+
return false;
|
|
20847
|
+
if (a && b && typeof a === "object") {
|
|
20848
|
+
if (Array.isArray(a) !== Array.isArray(b))
|
|
20849
|
+
return false;
|
|
20850
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
20851
|
+
if (a.length !== b.length)
|
|
20852
|
+
return false;
|
|
20853
|
+
return a.every((x, i) => deepEqual(x, b[i]));
|
|
20854
|
+
}
|
|
20855
|
+
const ao = a;
|
|
20856
|
+
const bo = b;
|
|
20857
|
+
const keys = new Set([...Object.keys(ao), ...Object.keys(bo)]);
|
|
20858
|
+
for (const k of keys)
|
|
20859
|
+
if (!deepEqual(ao[k], bo[k]))
|
|
20860
|
+
return false;
|
|
20861
|
+
return true;
|
|
20862
|
+
}
|
|
20863
|
+
return false;
|
|
20864
|
+
}
|
|
20865
|
+
function classifyBlastRadius(beforeYaml, afterYaml) {
|
|
20866
|
+
let before;
|
|
20867
|
+
let after;
|
|
20868
|
+
try {
|
|
20869
|
+
before = toObject2($parse(beforeYaml));
|
|
20870
|
+
after = toObject2($parse(afterYaml));
|
|
20871
|
+
} catch {
|
|
20872
|
+
return { agents: [], fleetWide: true, changedPaths: ["<unparseable>"] };
|
|
20873
|
+
}
|
|
20874
|
+
const changedPaths = changedConfigPaths(before, after).sort();
|
|
20875
|
+
if (changedPaths.length === 0) {
|
|
20876
|
+
return { agents: [], fleetWide: false, changedPaths: [] };
|
|
20877
|
+
}
|
|
20878
|
+
const agents = new Set;
|
|
20879
|
+
let fleetWide = false;
|
|
20880
|
+
for (const path2 of changedPaths) {
|
|
20881
|
+
if (path2 === "<root>") {
|
|
20882
|
+
fleetWide = true;
|
|
20883
|
+
continue;
|
|
20884
|
+
}
|
|
20885
|
+
const segs = path2.split(".");
|
|
20886
|
+
if (segs[0] === "agents" && segs.length >= 2) {
|
|
20887
|
+
agents.add(segs[1]);
|
|
20888
|
+
} else {
|
|
20889
|
+
fleetWide = true;
|
|
20890
|
+
}
|
|
20891
|
+
}
|
|
20892
|
+
return {
|
|
20893
|
+
agents: fleetWide ? [] : [...agents].sort(),
|
|
20894
|
+
fleetWide,
|
|
20895
|
+
changedPaths
|
|
20896
|
+
};
|
|
20897
|
+
}
|
|
20898
|
+
|
|
20820
20899
|
// src/host-control/server.ts
|
|
20821
20900
|
function resolveDigests(imageRefs) {
|
|
20822
20901
|
const out = new Map;
|
|
@@ -21509,7 +21588,12 @@ class HostdServer {
|
|
|
21509
21588
|
const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(["apply"]));
|
|
21510
21589
|
const recRes = await runner({ requestId: approvalId });
|
|
21511
21590
|
if (recRes.exit_code === 0) {
|
|
21512
|
-
|
|
21591
|
+
const blast = classifyBlastRadius(snapshot, postApply);
|
|
21592
|
+
await approval.finalize({
|
|
21593
|
+
outcome: "applied",
|
|
21594
|
+
affectedAgents: blast.agents,
|
|
21595
|
+
fleetWide: blast.fleetWide
|
|
21596
|
+
});
|
|
21513
21597
|
return {
|
|
21514
21598
|
v: 1,
|
|
21515
21599
|
request_id: req.request_id,
|
|
@@ -21893,7 +21977,9 @@ class SocketApprovalGateway {
|
|
|
21893
21977
|
type: "request_config_finalize",
|
|
21894
21978
|
requestId: req.requestId,
|
|
21895
21979
|
outcome: outcome.outcome,
|
|
21896
|
-
...outcome.detail ? { detail: outcome.detail } : {}
|
|
21980
|
+
...outcome.detail ? { detail: outcome.detail } : {},
|
|
21981
|
+
...outcome.affectedAgents ? { affectedAgents: outcome.affectedAgents } : {},
|
|
21982
|
+
...outcome.fleetWide !== undefined ? { fleetWide: outcome.fleetWide } : {}
|
|
21897
21983
|
}) + `
|
|
21898
21984
|
`);
|
|
21899
21985
|
client2.end();
|
package/package.json
CHANGED
|
@@ -31013,6 +31013,7 @@ __export(exports_config_approval_handler, {
|
|
|
31013
31013
|
parseConfigApprovalCallback: () => parseConfigApprovalCallback,
|
|
31014
31014
|
handleRequestConfigFinalize: () => handleRequestConfigFinalize,
|
|
31015
31015
|
handleRequestConfigApproval: () => handleRequestConfigApproval,
|
|
31016
|
+
buildLiveNote: () => buildLiveNote,
|
|
31016
31017
|
buildConfigApprovalCardBody: () => buildConfigApprovalCardBody,
|
|
31017
31018
|
_resetPendingConfigApprovalsForTest: () => _resetPendingConfigApprovalsForTest,
|
|
31018
31019
|
_peekPendingConfigApprovalForTest: () => _peekPendingConfigApprovalForTest,
|
|
@@ -31156,6 +31157,22 @@ async function resolvePendingConfigApproval(requestId, verdict, deps) {
|
|
|
31156
31157
|
}
|
|
31157
31158
|
return true;
|
|
31158
31159
|
}
|
|
31160
|
+
function buildLiveNote(affectedAgents, fleetWide) {
|
|
31161
|
+
if (fleetWide) {
|
|
31162
|
+
return `
|
|
31163
|
+
|
|
31164
|
+
\u26a0\ufe0f Shared config changed \u2014 affects all agents. Not live until they ` + `restart: run <code>switchroom rollout</code> (or <code>/update apply</code>).`;
|
|
31165
|
+
}
|
|
31166
|
+
const agents = (affectedAgents ?? []).filter((a) => typeof a === "string" && a.length > 0);
|
|
31167
|
+
if (agents.length === 0)
|
|
31168
|
+
return "";
|
|
31169
|
+
const list2 = agents.map(escapeHtml12).join(", ");
|
|
31170
|
+
const cmds = agents.map((a) => `/restart ${escapeHtml12(a)}`).join(" \u00b7 ");
|
|
31171
|
+
return `
|
|
31172
|
+
|
|
31173
|
+
\uD83D\uDD04 Not live until restart \u2014 affects: <b>${list2}</b>
|
|
31174
|
+
${cmds}`;
|
|
31175
|
+
}
|
|
31159
31176
|
async function handleRequestConfigFinalize(_client, msg, deps) {
|
|
31160
31177
|
const entry = pending.get(msg.requestId);
|
|
31161
31178
|
if (!entry) {
|
|
@@ -31163,8 +31180,9 @@ async function handleRequestConfigFinalize(_client, msg, deps) {
|
|
|
31163
31180
|
return;
|
|
31164
31181
|
}
|
|
31165
31182
|
pending.delete(msg.requestId);
|
|
31183
|
+
const liveNote = msg.outcome === "applied" ? buildLiveNote(msg.affectedAgents, msg.fleetWide) : "";
|
|
31166
31184
|
const body = msg.outcome === "applied" ? `\u2705 <b>Applied</b>${msg.detail ? `
|
|
31167
|
-
${escapeHtml12(msg.detail)}` : ""}` : `\u26a0\ufe0f <b>Reconcile failed; rolled back</b>${msg.detail ? `
|
|
31185
|
+
${escapeHtml12(msg.detail)}` : ""}${liveNote}` : `\u26a0\ufe0f <b>Reconcile failed; rolled back</b>${msg.detail ? `
|
|
31168
31186
|
${escapeHtml12(msg.detail)}` : ""}`;
|
|
31169
31187
|
try {
|
|
31170
31188
|
await deps.editCard({
|
|
@@ -45454,43 +45472,32 @@ async function handleEffortCommand(parsed, deps) {
|
|
|
45454
45472
|
const verbHtml = `<code>/effort ${deps.escapeHtml(parsed.level)}</code>`;
|
|
45455
45473
|
let result;
|
|
45456
45474
|
try {
|
|
45457
|
-
result = await deps.
|
|
45475
|
+
result = await deps.applyEffort(deps.getAgentName(), parsed.level);
|
|
45458
45476
|
} catch (err) {
|
|
45459
45477
|
const msg = err instanceof Error ? err.message : String(err);
|
|
45460
|
-
return { text: `\u274c ${verbHtml} \u2014
|
|
45478
|
+
return { text: `\u274c ${verbHtml} \u2014 failed: ${deps.escapeHtml(msg)}`, html: true };
|
|
45461
45479
|
}
|
|
45462
|
-
|
|
45463
|
-
|
|
45464
|
-
|
|
45465
|
-
|
|
45466
|
-
|
|
45467
|
-
|
|
45468
|
-
|
|
45469
|
-
|
|
45470
|
-
|
|
45471
|
-
|
|
45472
|
-
|
|
45480
|
+
return { text: applyResultText(parsed.level, result, deps), html: true };
|
|
45481
|
+
}
|
|
45482
|
+
function applyResultText(level, result, deps) {
|
|
45483
|
+
const verbHtml = `<code>/effort ${deps.escapeHtml(level)}</code>`;
|
|
45484
|
+
if (result.ok) {
|
|
45485
|
+
const lines = [`\u2705 ${verbHtml} \u2014 ${deps.escapeHtml(result.output)}`];
|
|
45486
|
+
if (result.confirmed) {
|
|
45487
|
+
lines.push("<i>Switched mid-conversation \u2014 your next turn re-reads the cached history (slower, one time).</i>");
|
|
45488
|
+
}
|
|
45489
|
+
lines.push(PERSIST_NOTE2);
|
|
45490
|
+
return lines.join(`
|
|
45491
|
+
`);
|
|
45473
45492
|
}
|
|
45474
|
-
if (result.
|
|
45475
|
-
return
|
|
45476
|
-
text: [
|
|
45477
|
-
`${verbHtml} \u2014 sent, but no response captured. The agent may be mid-turn; check <code>/inject /status</code> to confirm the active effort.`,
|
|
45478
|
-
PERSIST_NOTE2
|
|
45479
|
-
].join(`
|
|
45480
|
-
`),
|
|
45481
|
-
html: true
|
|
45482
|
-
};
|
|
45493
|
+
if (result.reason === "session_missing") {
|
|
45494
|
+
return "\u274c tmux session not found \u2014 the agent must be running under the tmux supervisor (the default). Remove <code>experimental.legacy_pty: true</code> if set.";
|
|
45483
45495
|
}
|
|
45484
|
-
if (result.
|
|
45485
|
-
|
|
45486
|
-
|
|
45487
|
-
html: true
|
|
45488
|
-
};
|
|
45496
|
+
if (result.reason === "confirm_failed") {
|
|
45497
|
+
const wedged = result.wedged ? " The confirmation prompt may still be open on the pane \u2014 check it." : " The change was cancelled and the pane left as it was.";
|
|
45498
|
+
return `\u274c ${verbHtml} \u2014 couldn't confirm the switch.${wedged}`;
|
|
45489
45499
|
}
|
|
45490
|
-
return {
|
|
45491
|
-
text: `\u274c ${verbHtml} \u2014 ${deps.escapeHtml(result.errorMessage ?? "inject failed")}`,
|
|
45492
|
-
html: true
|
|
45493
|
-
};
|
|
45500
|
+
return `\u274c ${verbHtml} \u2014 sent, but couldn't confirm it applied. The agent may be mid-turn; check <code>/inject /status</code>.`;
|
|
45494
45501
|
}
|
|
45495
45502
|
var EFFORT_CALLBACK_PREFIX = "eff:";
|
|
45496
45503
|
var EFFORT_CALLBACK_SELECT = "eff:s:";
|
|
@@ -45531,18 +45538,20 @@ async function handleEffortMenuCallback(data, deps) {
|
|
|
45531
45538
|
let banner;
|
|
45532
45539
|
let selected;
|
|
45533
45540
|
try {
|
|
45534
|
-
const result = await deps.
|
|
45535
|
-
if (result.
|
|
45536
|
-
banner = `\u2705 Effort \u2192 <code>${deps.escapeHtml(level)}</code> for this session`;
|
|
45541
|
+
const result = await deps.applyEffort(deps.getAgentName(), level);
|
|
45542
|
+
if (result.ok) {
|
|
45543
|
+
banner = result.confirmed ? `\u2705 Effort \u2192 <code>${deps.escapeHtml(level)}</code> (mid-conversation: next turn re-reads history)` : `\u2705 Effort \u2192 <code>${deps.escapeHtml(level)}</code> for this session`;
|
|
45537
45544
|
selected = level;
|
|
45538
|
-
} else if (result.
|
|
45545
|
+
} else if (result.reason === "session_missing") {
|
|
45539
45546
|
banner = "\u274c tmux session not found \u2014 is the agent running under the supervisor?";
|
|
45547
|
+
} else if (result.reason === "confirm_failed") {
|
|
45548
|
+
banner = result.wedged ? "\u26a0\ufe0f Couldn\u2019t confirm the switch \u2014 the prompt may still be open on the pane." : "\u274c Couldn\u2019t confirm the switch \u2014 cancelled, effort unchanged.";
|
|
45540
45549
|
} else {
|
|
45541
|
-
banner =
|
|
45550
|
+
banner = "\u274c Sent, but couldn\u2019t confirm it applied (agent may be mid-turn).";
|
|
45542
45551
|
}
|
|
45543
45552
|
} catch (err) {
|
|
45544
45553
|
const msg = err instanceof Error ? err.message : String(err);
|
|
45545
|
-
banner = `\u274c
|
|
45554
|
+
banner = `\u274c failed: ${deps.escapeHtml(msg)}`;
|
|
45546
45555
|
}
|
|
45547
45556
|
const menu = buildEffortMenu(deps, selected);
|
|
45548
45557
|
return {
|
|
@@ -45552,6 +45561,80 @@ ${menu.text}` },
|
|
|
45552
45561
|
};
|
|
45553
45562
|
}
|
|
45554
45563
|
|
|
45564
|
+
// ../src/agents/effort-picker.ts
|
|
45565
|
+
var CONFIRM_RE = /Change effort level\?/i;
|
|
45566
|
+
function appliedRe(level) {
|
|
45567
|
+
return new RegExp(`${level}\\s*\\u00b7\\s*/effort`);
|
|
45568
|
+
}
|
|
45569
|
+
function applyLine(pane, level) {
|
|
45570
|
+
const re = new RegExp(`Set effort level to ${level}\\b.*`, "i");
|
|
45571
|
+
for (const line of pane.split(`
|
|
45572
|
+
`)) {
|
|
45573
|
+
const m = line.match(re);
|
|
45574
|
+
if (m)
|
|
45575
|
+
return m[0].trim();
|
|
45576
|
+
}
|
|
45577
|
+
return `Set effort level to ${level}`;
|
|
45578
|
+
}
|
|
45579
|
+
var realSleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
45580
|
+
async function applyEffort(agentName3, level, opts = {}) {
|
|
45581
|
+
const runner = opts._runner ?? makeTmuxRunner2(opts.tmuxBin ?? "tmux");
|
|
45582
|
+
const socket = opts.socketName ?? `switchroom-${agentName3}`;
|
|
45583
|
+
const session = opts.sessionName ?? agentName3;
|
|
45584
|
+
const stepMs = opts.stepMs ?? 600;
|
|
45585
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
45586
|
+
const sleep2 = opts._sleep ?? realSleep2;
|
|
45587
|
+
const log = opts._log ?? ((line) => process.stderr.write(`${line}
|
|
45588
|
+
`));
|
|
45589
|
+
if (!runner.hasSession(socket, session)) {
|
|
45590
|
+
return { ok: false, reason: "session_missing" };
|
|
45591
|
+
}
|
|
45592
|
+
return withPaneLock(`${socket}:${session}`, async () => {
|
|
45593
|
+
const startedAt = Date.now();
|
|
45594
|
+
const expired2 = () => Date.now() - startedAt >= timeoutMs;
|
|
45595
|
+
try {
|
|
45596
|
+
runner.send(socket, session, ["send-keys", "-l", `/effort ${level}`]);
|
|
45597
|
+
runner.send(socket, session, ["send-keys", "Enter"]);
|
|
45598
|
+
let confirmed = false;
|
|
45599
|
+
let confirmKeys = 0;
|
|
45600
|
+
while (!expired2()) {
|
|
45601
|
+
await sleep2(stepMs);
|
|
45602
|
+
const pane = runner.capture(socket, session) ?? "";
|
|
45603
|
+
if (CONFIRM_RE.test(pane)) {
|
|
45604
|
+
if (confirmKeys >= 2) {
|
|
45605
|
+
runner.send(socket, session, ["send-keys", "Escape"]);
|
|
45606
|
+
await sleep2(stepMs);
|
|
45607
|
+
const after = runner.capture(socket, session) ?? "";
|
|
45608
|
+
log(`effort-picker: confirm modal would not dismiss for ${agentName3} ` + `(socket=${socket}) \u2014 cancelled`);
|
|
45609
|
+
return { ok: false, reason: "confirm_failed", wedged: CONFIRM_RE.test(after) };
|
|
45610
|
+
}
|
|
45611
|
+
runner.send(socket, session, ["send-keys", "Enter"]);
|
|
45612
|
+
confirmed = true;
|
|
45613
|
+
confirmKeys += 1;
|
|
45614
|
+
continue;
|
|
45615
|
+
}
|
|
45616
|
+
if (appliedRe(level).test(pane)) {
|
|
45617
|
+
return { ok: true, level, confirmed, output: applyLine(pane, level) };
|
|
45618
|
+
}
|
|
45619
|
+
}
|
|
45620
|
+
const final = runner.capture(socket, session) ?? "";
|
|
45621
|
+
if (CONFIRM_RE.test(final)) {
|
|
45622
|
+
runner.send(socket, session, ["send-keys", "Escape"]);
|
|
45623
|
+
log(`effort-picker: timeout with modal open for ${agentName3} \u2014 cancelled`);
|
|
45624
|
+
return { ok: false, reason: "confirm_failed", wedged: true };
|
|
45625
|
+
}
|
|
45626
|
+
return { ok: false, reason: "apply_unverified" };
|
|
45627
|
+
} finally {
|
|
45628
|
+
try {
|
|
45629
|
+
const pane = runner.capture(socket, session) ?? "";
|
|
45630
|
+
if (CONFIRM_RE.test(pane)) {
|
|
45631
|
+
runner.send(socket, session, ["send-keys", "Escape"]);
|
|
45632
|
+
}
|
|
45633
|
+
} catch {}
|
|
45634
|
+
}
|
|
45635
|
+
});
|
|
45636
|
+
}
|
|
45637
|
+
|
|
45555
45638
|
// ../src/config/loader.ts
|
|
45556
45639
|
init_dist();
|
|
45557
45640
|
init_zod();
|
|
@@ -47576,6 +47659,16 @@ function validateClientMessage(msg) {
|
|
|
47576
47659
|
return false;
|
|
47577
47660
|
if (m.detail !== undefined && (typeof m.detail !== "string" || m.detail.length > 500))
|
|
47578
47661
|
return false;
|
|
47662
|
+
if (m.affectedAgents !== undefined) {
|
|
47663
|
+
if (!Array.isArray(m.affectedAgents) || m.affectedAgents.length > 64)
|
|
47664
|
+
return false;
|
|
47665
|
+
for (const a of m.affectedAgents) {
|
|
47666
|
+
if (typeof a !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(a))
|
|
47667
|
+
return false;
|
|
47668
|
+
}
|
|
47669
|
+
}
|
|
47670
|
+
if (m.fleetWide !== undefined && typeof m.fleetWide !== "boolean")
|
|
47671
|
+
return false;
|
|
47579
47672
|
return true;
|
|
47580
47673
|
}
|
|
47581
47674
|
case "request_drive_approval": {
|
|
@@ -54327,11 +54420,11 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
|
|
|
54327
54420
|
}
|
|
54328
54421
|
|
|
54329
54422
|
// ../src/build-info.ts
|
|
54330
|
-
var VERSION = "0.15.
|
|
54331
|
-
var COMMIT_SHA = "
|
|
54332
|
-
var COMMIT_DATE = "2026-06-
|
|
54333
|
-
var LATEST_PR =
|
|
54334
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
54423
|
+
var VERSION = "0.15.22";
|
|
54424
|
+
var COMMIT_SHA = "a6c13429";
|
|
54425
|
+
var COMMIT_DATE = "2026-06-14T03:27:21Z";
|
|
54426
|
+
var LATEST_PR = 2349;
|
|
54427
|
+
var COMMITS_AHEAD_OF_TAG = 0;
|
|
54335
54428
|
|
|
54336
54429
|
// gateway/boot-version.ts
|
|
54337
54430
|
function formatRelativeAgo(iso) {
|
|
@@ -55829,6 +55922,23 @@ function formatFeedElapsed(ms) {
|
|
|
55829
55922
|
function turnInFlightForGate() {
|
|
55830
55923
|
return isDeliveryCutoverEnabled() ? isMachineInTurn() : claudeBusyKeys.size > 0;
|
|
55831
55924
|
}
|
|
55925
|
+
function deliverResumeSyntheticOrBuffer(agent, inbound) {
|
|
55926
|
+
const decision = decideInboundDelivery({
|
|
55927
|
+
turnInFlight: turnInFlightForGate(),
|
|
55928
|
+
isSteering: false,
|
|
55929
|
+
isInterrupt: false
|
|
55930
|
+
});
|
|
55931
|
+
if (decision === "buffer-until-idle") {
|
|
55932
|
+
pendingInboundBuffer.push(agent, inbound);
|
|
55933
|
+
return false;
|
|
55934
|
+
}
|
|
55935
|
+
const delivered = ipcServer.sendToAgent(agent, inbound);
|
|
55936
|
+
if (delivered)
|
|
55937
|
+
markClaudeBusyForInbound(inbound);
|
|
55938
|
+
else
|
|
55939
|
+
pendingInboundBuffer.push(agent, inbound);
|
|
55940
|
+
return delivered;
|
|
55941
|
+
}
|
|
55832
55942
|
var pendingRestarts = new Map;
|
|
55833
55943
|
var lastSessionActiveFile = null;
|
|
55834
55944
|
var compactState = initialCompactState();
|
|
@@ -59311,11 +59421,7 @@ async function captureProvidedSecret(ctx, chat_id, msgId, value) {
|
|
|
59311
59421
|
stage_id: armed.stageId
|
|
59312
59422
|
}
|
|
59313
59423
|
};
|
|
59314
|
-
|
|
59315
|
-
if (fdelivered)
|
|
59316
|
-
markClaudeBusyForInbound(failMsg);
|
|
59317
|
-
else
|
|
59318
|
-
pendingInboundBuffer.push(armed.agent, failMsg);
|
|
59424
|
+
deliverResumeSyntheticOrBuffer(armed.agent, failMsg);
|
|
59319
59425
|
return true;
|
|
59320
59426
|
}
|
|
59321
59427
|
await switchroomReply(ctx, `\u2705 saved as <code>vault:${escapeHtmlForTg(armed.key)}</code> (masked: <code>${escapeHtmlForTg(maskToken2(value))}</code>). The agent can now reference it.`, { html: true });
|
|
@@ -59337,11 +59443,7 @@ async function captureProvidedSecret(ctx, chat_id, msgId, value) {
|
|
|
59337
59443
|
stage_id: armed.stageId
|
|
59338
59444
|
}
|
|
59339
59445
|
};
|
|
59340
|
-
const delivered =
|
|
59341
|
-
if (delivered)
|
|
59342
|
-
markClaudeBusyForInbound(synthetic);
|
|
59343
|
-
else
|
|
59344
|
-
pendingInboundBuffer.push(armed.agent, synthetic);
|
|
59446
|
+
const delivered = deliverResumeSyntheticOrBuffer(armed.agent, synthetic);
|
|
59345
59447
|
process.stderr.write(`telegram gateway: secret_provided injection agent=${armed.agent} key=${armed.key} stage=${armed.stageId} delivered=${delivered}
|
|
59346
59448
|
`);
|
|
59347
59449
|
return true;
|
|
@@ -59403,11 +59505,7 @@ async function handleSecretRequestCallback(ctx, data) {
|
|
|
59403
59505
|
stage_id: stageId
|
|
59404
59506
|
}
|
|
59405
59507
|
};
|
|
59406
|
-
|
|
59407
|
-
if (delivered)
|
|
59408
|
-
markClaudeBusyForInbound(synthetic);
|
|
59409
|
-
else
|
|
59410
|
-
pendingInboundBuffer.push(pending2.agent, synthetic);
|
|
59508
|
+
deliverResumeSyntheticOrBuffer(pending2.agent, synthetic);
|
|
59411
59509
|
return;
|
|
59412
59510
|
}
|
|
59413
59511
|
await ctx.answerCallbackQuery().catch(() => {});
|
|
@@ -62334,14 +62432,13 @@ bot.command("model", async (ctx) => {
|
|
|
62334
62432
|
});
|
|
62335
62433
|
function buildEffortDeps() {
|
|
62336
62434
|
return {
|
|
62337
|
-
|
|
62435
|
+
applyEffort: (agent, level) => applyEffort(agent, level),
|
|
62338
62436
|
getAgentName: getMyAgentName,
|
|
62339
62437
|
getConfiguredEffort: () => {
|
|
62340
62438
|
const data = switchroomExecJson(["agent", "list"]);
|
|
62341
62439
|
return data?.agents?.find((a) => a.name === getMyAgentName())?.thinking_effort ?? null;
|
|
62342
62440
|
},
|
|
62343
|
-
escapeHtml: escapeHtmlForTg
|
|
62344
|
-
preBlock
|
|
62441
|
+
escapeHtml: escapeHtmlForTg
|
|
62345
62442
|
};
|
|
62346
62443
|
}
|
|
62347
62444
|
function effortMenuReplyMarkup(reply) {
|
|
@@ -63608,14 +63705,9 @@ async function performVaultAccessApproval(ctx, pending2, stageId, senderId, atte
|
|
|
63608
63705
|
stageId,
|
|
63609
63706
|
operatorId: senderId
|
|
63610
63707
|
});
|
|
63611
|
-
const delivered =
|
|
63612
|
-
if (delivered)
|
|
63613
|
-
markClaudeBusyForInbound(synthetic);
|
|
63708
|
+
const delivered = deliverResumeSyntheticOrBuffer(pending2.agent, synthetic);
|
|
63614
63709
|
process.stderr.write(`telegram gateway: vault_grant_approved injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${delivered}
|
|
63615
63710
|
`);
|
|
63616
|
-
if (!delivered) {
|
|
63617
|
-
pendingInboundBuffer.push(pending2.agent, synthetic);
|
|
63618
|
-
}
|
|
63619
63711
|
}
|
|
63620
63712
|
async function handleVaultRequestAccessCallback(ctx, data) {
|
|
63621
63713
|
const senderId = String(ctx.from?.id ?? "");
|
|
@@ -63657,14 +63749,9 @@ async function handleVaultRequestAccessCallback(ctx, data) {
|
|
|
63657
63749
|
stageId,
|
|
63658
63750
|
operatorId: senderId
|
|
63659
63751
|
});
|
|
63660
|
-
const denyDelivered =
|
|
63661
|
-
if (denyDelivered)
|
|
63662
|
-
markClaudeBusyForInbound(denyInbound);
|
|
63752
|
+
const denyDelivered = deliverResumeSyntheticOrBuffer(pending2.agent, denyInbound);
|
|
63663
63753
|
process.stderr.write(`telegram gateway: vault_grant_denied injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${denyDelivered}
|
|
63664
63754
|
`);
|
|
63665
|
-
if (!denyDelivered) {
|
|
63666
|
-
pendingInboundBuffer.push(pending2.agent, denyInbound);
|
|
63667
|
-
}
|
|
63668
63755
|
return;
|
|
63669
63756
|
}
|
|
63670
63757
|
if (action === "approve") {
|
|
@@ -63756,13 +63843,9 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
63756
63843
|
stageId,
|
|
63757
63844
|
operatorId: senderId
|
|
63758
63845
|
});
|
|
63759
|
-
const dDelivered =
|
|
63760
|
-
if (dDelivered)
|
|
63761
|
-
markClaudeBusyForInbound(discardInbound);
|
|
63846
|
+
const dDelivered = deliverResumeSyntheticOrBuffer(pending2.agent, discardInbound);
|
|
63762
63847
|
process.stderr.write(`telegram gateway: vault_save_discarded injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${dDelivered}
|
|
63763
63848
|
`);
|
|
63764
|
-
if (!dDelivered)
|
|
63765
|
-
pendingInboundBuffer.push(pending2.agent, discardInbound);
|
|
63766
63849
|
return;
|
|
63767
63850
|
}
|
|
63768
63851
|
if (action === "rename") {
|
|
@@ -63823,13 +63906,9 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
63823
63906
|
operatorId: senderId,
|
|
63824
63907
|
reason: failReason
|
|
63825
63908
|
});
|
|
63826
|
-
const fDelivered =
|
|
63827
|
-
if (fDelivered)
|
|
63828
|
-
markClaudeBusyForInbound(failInbound);
|
|
63909
|
+
const fDelivered = deliverResumeSyntheticOrBuffer(pending2.agent, failInbound);
|
|
63829
63910
|
process.stderr.write(`telegram gateway: vault_save_failed injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${fDelivered}
|
|
63830
63911
|
`);
|
|
63831
|
-
if (!fDelivered)
|
|
63832
|
-
pendingInboundBuffer.push(pending2.agent, failInbound);
|
|
63833
63912
|
return;
|
|
63834
63913
|
}
|
|
63835
63914
|
pendingVaultRequestSaves.delete(stageId);
|
|
@@ -63847,13 +63926,9 @@ async function handleVaultRequestSaveCallback(ctx, data) {
|
|
|
63847
63926
|
stageId,
|
|
63848
63927
|
operatorId: senderId
|
|
63849
63928
|
});
|
|
63850
|
-
const okDelivered =
|
|
63851
|
-
if (okDelivered)
|
|
63852
|
-
markClaudeBusyForInbound(okInbound);
|
|
63929
|
+
const okDelivered = deliverResumeSyntheticOrBuffer(pending2.agent, okInbound);
|
|
63853
63930
|
process.stderr.write(`telegram gateway: vault_save_completed injection agent=${pending2.agent} key=${pending2.key} stage=${stageId} delivered=${okDelivered}
|
|
63854
63931
|
`);
|
|
63855
|
-
if (!okDelivered)
|
|
63856
|
-
pendingInboundBuffer.push(pending2.agent, okInbound);
|
|
63857
63932
|
return;
|
|
63858
63933
|
}
|
|
63859
63934
|
await ctx.answerCallbackQuery({ text: "Unknown action" }).catch(() => {});
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
14
14
|
import {
|
|
15
15
|
buildConfigApprovalCardBody,
|
|
16
|
+
buildLiveNote,
|
|
16
17
|
handleRequestConfigApproval,
|
|
17
18
|
handleRequestConfigFinalize,
|
|
18
19
|
parseConfigApprovalCallback,
|
|
@@ -266,6 +267,29 @@ describe("timeout path", () => {
|
|
|
266
267
|
});
|
|
267
268
|
});
|
|
268
269
|
|
|
270
|
+
describe("buildLiveNote", () => {
|
|
271
|
+
it("names specific affected agents + the per-agent restart command", () => {
|
|
272
|
+
const note = buildLiveNote(["clerk", "gymbro"], false);
|
|
273
|
+
expect(note).toContain("clerk, gymbro");
|
|
274
|
+
expect(note).toContain("/restart clerk");
|
|
275
|
+
expect(note).toContain("/restart gymbro");
|
|
276
|
+
expect(note).toContain("Not live until restart");
|
|
277
|
+
});
|
|
278
|
+
it("guides to a full rollout when fleet-wide (no per-agent list)", () => {
|
|
279
|
+
const note = buildLiveNote([], true);
|
|
280
|
+
expect(note).toContain("all agents");
|
|
281
|
+
expect(note).toContain("switchroom rollout");
|
|
282
|
+
expect(note).not.toContain("/restart");
|
|
283
|
+
});
|
|
284
|
+
it("is empty when nothing is runtime-affected", () => {
|
|
285
|
+
expect(buildLiveNote([], false)).toBe("");
|
|
286
|
+
expect(buildLiveNote(undefined, undefined)).toBe("");
|
|
287
|
+
});
|
|
288
|
+
it("HTML-escapes agent names", () => {
|
|
289
|
+
expect(buildLiveNote(["a<b>"], false)).toContain("a<b>");
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
269
293
|
describe("handleRequestConfigFinalize", () => {
|
|
270
294
|
it("edits the card to '✅ Applied' on success", async () => {
|
|
271
295
|
const { client, deps, editCalls } = fakeDeps();
|
|
@@ -377,6 +377,27 @@ export async function resolvePendingConfigApproval(
|
|
|
377
377
|
return true;
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
/**
|
|
381
|
+
* The "make it live" note appended to an Applied card. claude loads config at
|
|
382
|
+
* boot, so an applied edit is inert in the running agents until they restart —
|
|
383
|
+
* this names exactly what must bounce (and the command) instead of letting the
|
|
384
|
+
* change silently not take effect. Fleet-wide (shared config) → guide to a full
|
|
385
|
+
* rollout, never a per-agent list. Empty when nothing runtime-affected.
|
|
386
|
+
*/
|
|
387
|
+
export function buildLiveNote(affectedAgents?: string[], fleetWide?: boolean): string {
|
|
388
|
+
if (fleetWide) {
|
|
389
|
+
return (
|
|
390
|
+
`\n\n⚠️ Shared config changed — affects all agents. Not live until they ` +
|
|
391
|
+
`restart: run <code>switchroom rollout</code> (or <code>/update apply</code>).`
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
const agents = (affectedAgents ?? []).filter((a) => typeof a === "string" && a.length > 0);
|
|
395
|
+
if (agents.length === 0) return "";
|
|
396
|
+
const list = agents.map(escapeHtml).join(", ");
|
|
397
|
+
const cmds = agents.map((a) => `/restart ${escapeHtml(a)}`).join(" · ");
|
|
398
|
+
return `\n\n🔄 Not live until restart — affects: <b>${list}</b>\n${cmds}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
380
401
|
/** IPC `request_config_finalize` handler — edits the card to the terminal outcome. */
|
|
381
402
|
export async function handleRequestConfigFinalize(
|
|
382
403
|
_client: Pick<IpcClient, "send">,
|
|
@@ -393,9 +414,15 @@ export async function handleRequestConfigFinalize(
|
|
|
393
414
|
// Clean up the pending entry — finalize is the terminal transition.
|
|
394
415
|
pending.delete(msg.requestId);
|
|
395
416
|
|
|
417
|
+
// On apply, tell the operator what must restart for the edit to go LIVE —
|
|
418
|
+
// claude loads config at boot, so an applied edit is inert until restart.
|
|
419
|
+
// Specific agents → name them + the one-liner to bounce them; shared config
|
|
420
|
+
// → guide to a full rollout (never silently leave the change un-live).
|
|
421
|
+
const liveNote =
|
|
422
|
+
msg.outcome === "applied" ? buildLiveNote(msg.affectedAgents, msg.fleetWide) : "";
|
|
396
423
|
const body =
|
|
397
424
|
msg.outcome === "applied"
|
|
398
|
-
? `✅ <b>Applied</b>${msg.detail ? `\n${escapeHtml(msg.detail)}` : ""}`
|
|
425
|
+
? `✅ <b>Applied</b>${msg.detail ? `\n${escapeHtml(msg.detail)}` : ""}${liveNote}`
|
|
399
426
|
: `⚠️ <b>Reconcile failed; rolled back</b>${msg.detail ? `\n${escapeHtml(msg.detail)}` : ""}`;
|
|
400
427
|
try {
|
|
401
428
|
await deps.editCard({
|