switchroom 0.12.23 → 0.12.25
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
CHANGED
|
@@ -47247,8 +47247,8 @@ var {
|
|
|
47247
47247
|
} = import__.default;
|
|
47248
47248
|
|
|
47249
47249
|
// src/build-info.ts
|
|
47250
|
-
var VERSION = "0.12.
|
|
47251
|
-
var COMMIT_SHA = "
|
|
47250
|
+
var VERSION = "0.12.25";
|
|
47251
|
+
var COMMIT_SHA = "4b8ab51";
|
|
47252
47252
|
|
|
47253
47253
|
// src/cli/agent.ts
|
|
47254
47254
|
init_source();
|
package/package.json
CHANGED
|
@@ -27253,32 +27253,32 @@ function formatAuthQuotaLine(acc, now = Date.now()) {
|
|
|
27253
27253
|
}
|
|
27254
27254
|
return null;
|
|
27255
27255
|
}
|
|
27256
|
-
function renderAuthLine(
|
|
27257
|
-
if (!
|
|
27256
|
+
function renderAuthLine(state4, agentName3, now = Date.now()) {
|
|
27257
|
+
if (!state4 || state4.accounts.length === 0)
|
|
27258
27258
|
return [];
|
|
27259
|
-
const agentEntry =
|
|
27260
|
-
const activeLabel = agentEntry?.override ?? agentEntry?.account ??
|
|
27259
|
+
const agentEntry = state4.agents.find((a) => a.name === agentName3);
|
|
27260
|
+
const activeLabel = agentEntry?.override ?? agentEntry?.account ?? state4.active;
|
|
27261
27261
|
const seen = new Set;
|
|
27262
27262
|
const order = [];
|
|
27263
27263
|
if (activeLabel) {
|
|
27264
27264
|
order.push(activeLabel);
|
|
27265
27265
|
seen.add(activeLabel);
|
|
27266
27266
|
}
|
|
27267
|
-
for (const label of
|
|
27267
|
+
for (const label of state4.fallback_order) {
|
|
27268
27268
|
if (!seen.has(label)) {
|
|
27269
27269
|
order.push(label);
|
|
27270
27270
|
seen.add(label);
|
|
27271
27271
|
}
|
|
27272
27272
|
}
|
|
27273
|
-
for (const acc of
|
|
27273
|
+
for (const acc of state4.accounts) {
|
|
27274
27274
|
if (!seen.has(acc.label)) {
|
|
27275
27275
|
order.push(acc.label);
|
|
27276
27276
|
seen.add(acc.label);
|
|
27277
27277
|
}
|
|
27278
27278
|
}
|
|
27279
|
-
const byLabel = new Map(
|
|
27279
|
+
const byLabel = new Map(state4.accounts.map((a) => [a.label, a]));
|
|
27280
27280
|
const rows = [];
|
|
27281
|
-
rows.push(`<b>Accounts (${
|
|
27281
|
+
rows.push(`<b>Accounts (${state4.accounts.length})</b>`);
|
|
27282
27282
|
for (const label of order) {
|
|
27283
27283
|
const acc = byLabel.get(label);
|
|
27284
27284
|
if (!acc)
|
|
@@ -27540,15 +27540,15 @@ function uptimeMsForStarttime(starttimeTicks, fs2 = realProcFs) {
|
|
|
27540
27540
|
return null;
|
|
27541
27541
|
}
|
|
27542
27542
|
}
|
|
27543
|
-
function nextStepForAgentState(agentName3,
|
|
27544
|
-
if (
|
|
27543
|
+
function nextStepForAgentState(agentName3, state4) {
|
|
27544
|
+
if (state4 === "failed") {
|
|
27545
27545
|
return `Service failed \u2014 inspect with \`journalctl --user -u switchroom-${agentName3} -n 100\` then \`switchroom agent restart ${agentName3}\``;
|
|
27546
27546
|
}
|
|
27547
|
-
if (
|
|
27547
|
+
if (state4 === "inactive") {
|
|
27548
27548
|
return `Service inactive \u2014 start with \`switchroom agent start ${agentName3}\` (or \`systemctl --user start switchroom-${agentName3}\`)`;
|
|
27549
27549
|
}
|
|
27550
|
-
if (
|
|
27551
|
-
return `Service is in a transient \`${
|
|
27550
|
+
if (state4 === "deactivating" || state4 === "activating" || state4 === "auto-restart") {
|
|
27551
|
+
return `Service is in a transient \`${state4}\` state \u2014 re-check with \`switchroom agent status ${agentName3}\` in a few seconds`;
|
|
27552
27552
|
}
|
|
27553
27553
|
return `Inspect with \`journalctl --user -u switchroom-${agentName3} -n 100\``;
|
|
27554
27554
|
}
|
|
@@ -27670,8 +27670,8 @@ async function probeAgentProcess(agentName3, opts = {}) {
|
|
|
27670
27670
|
if ("error" in snapshot) {
|
|
27671
27671
|
return { status: "fail", label: "Agent", detail: snapshot.error };
|
|
27672
27672
|
}
|
|
27673
|
-
const { state:
|
|
27674
|
-
if (
|
|
27673
|
+
const { state: state4, kv } = snapshot;
|
|
27674
|
+
if (state4 === "active") {
|
|
27675
27675
|
let pid = kv["MainPID"] ?? "?";
|
|
27676
27676
|
if (opts.tmuxSupervisor) {
|
|
27677
27677
|
const resolved = await resolveTmuxSupervisorPid(agentName3, execFileFn);
|
|
@@ -27685,10 +27685,10 @@ async function probeAgentProcess(agentName3, opts = {}) {
|
|
|
27685
27685
|
}
|
|
27686
27686
|
const elapsedMs = Date.now() - startMs;
|
|
27687
27687
|
if (elapsedMs >= retryMaxMs) {
|
|
27688
|
-
const isTransient =
|
|
27688
|
+
const isTransient = state4 === "deactivating" || state4 === "activating" || state4 === "auto-restart";
|
|
27689
27689
|
const status = isTransient ? "degraded" : "fail";
|
|
27690
|
-
const nextStep = nextStepForAgentState(agentName3,
|
|
27691
|
-
return { status, label: "Agent", detail: `service ${
|
|
27690
|
+
const nextStep = nextStepForAgentState(agentName3, state4);
|
|
27691
|
+
return { status, label: "Agent", detail: `service ${state4}`, ...nextStep ? { nextStep } : {} };
|
|
27692
27692
|
}
|
|
27693
27693
|
await sleep2(retryIntervalMs);
|
|
27694
27694
|
}
|
|
@@ -27708,8 +27708,8 @@ async function* watchAgentProcess(agentName3, opts = {}) {
|
|
|
27708
27708
|
const now = opts.nowImpl ?? (() => Date.now());
|
|
27709
27709
|
const startMs = now();
|
|
27710
27710
|
let lastYieldedDetail = null;
|
|
27711
|
-
async function toProbeResult(
|
|
27712
|
-
if (
|
|
27711
|
+
async function toProbeResult(state4, kv, withinWindow) {
|
|
27712
|
+
if (state4 === "active") {
|
|
27713
27713
|
let pid = kv["MainPID"] ?? "?";
|
|
27714
27714
|
if (opts.tmuxSupervisor) {
|
|
27715
27715
|
const resolved = await resolveTmuxSupervisorPid(agentName3, execFileFn);
|
|
@@ -27722,15 +27722,15 @@ async function* watchAgentProcess(agentName3, opts = {}) {
|
|
|
27722
27722
|
return { status: "ok", label: "Agent", detail: parts.join(" \u00b7 ") };
|
|
27723
27723
|
}
|
|
27724
27724
|
if (withinWindow) {
|
|
27725
|
-
if (
|
|
27725
|
+
if (state4 === "failed") {
|
|
27726
27726
|
return { status: "fail", label: "Agent", detail: "service failed" };
|
|
27727
27727
|
}
|
|
27728
27728
|
return { status: "degraded", label: "Agent", detail: "service starting" };
|
|
27729
27729
|
}
|
|
27730
|
-
const isTransient =
|
|
27730
|
+
const isTransient = state4 === "deactivating" || state4 === "activating" || state4 === "auto-restart" || state4 === "inactive";
|
|
27731
27731
|
const status = isTransient ? "degraded" : "fail";
|
|
27732
|
-
const nextStep = nextStepForAgentState(agentName3,
|
|
27733
|
-
return { status, label: "Agent", detail: `service ${
|
|
27732
|
+
const nextStep = nextStepForAgentState(agentName3, state4);
|
|
27733
|
+
return { status, label: "Agent", detail: `service ${state4}`, ...nextStep ? { nextStep } : {} };
|
|
27734
27734
|
}
|
|
27735
27735
|
while (true) {
|
|
27736
27736
|
const elapsedMs = now() - startMs;
|
|
@@ -29553,14 +29553,14 @@ function switchPriority2(s) {
|
|
|
29553
29553
|
function escapeHtml12(s) {
|
|
29554
29554
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
29555
29555
|
}
|
|
29556
|
-
function buildSnapshotsFromState2(
|
|
29556
|
+
function buildSnapshotsFromState2(state4, quotas) {
|
|
29557
29557
|
const out = [];
|
|
29558
|
-
for (let i = 0;i <
|
|
29559
|
-
const acc =
|
|
29558
|
+
for (let i = 0;i < state4.accounts.length; i++) {
|
|
29559
|
+
const acc = state4.accounts[i];
|
|
29560
29560
|
const q = quotas[i];
|
|
29561
29561
|
out.push({
|
|
29562
29562
|
label: acc.label,
|
|
29563
|
-
isActive: acc.label ===
|
|
29563
|
+
isActive: acc.label === state4.active,
|
|
29564
29564
|
quota: q && q.ok ? q.data : null,
|
|
29565
29565
|
quotaError: q && !q.ok ? q.reason : undefined,
|
|
29566
29566
|
expiresAtMs: acc.expiresAt
|
|
@@ -43814,6 +43814,237 @@ function chatKeyWithSuffix(chatId, threadId, suffix) {
|
|
|
43814
43814
|
return `${chatKey(chatId, threadId)}:${suffix}`;
|
|
43815
43815
|
}
|
|
43816
43816
|
|
|
43817
|
+
// gateway/inbound-delivery-machine.ts
|
|
43818
|
+
function initialState() {
|
|
43819
|
+
return {
|
|
43820
|
+
global: { kind: "bridge_dead" },
|
|
43821
|
+
perKey: new Map
|
|
43822
|
+
};
|
|
43823
|
+
}
|
|
43824
|
+
var TURN_TTL_MS = 300000;
|
|
43825
|
+
var OUTBOUND_RECENT_MS = 60000;
|
|
43826
|
+
function emptyPerKey() {
|
|
43827
|
+
return { turnStartedAt: null, lastOutboundAt: null };
|
|
43828
|
+
}
|
|
43829
|
+
function updatePerKey(state3, key, update) {
|
|
43830
|
+
const prior = state3.perKey.get(key) ?? emptyPerKey();
|
|
43831
|
+
const next = update(prior);
|
|
43832
|
+
const m = new Map(state3.perKey);
|
|
43833
|
+
if (next.turnStartedAt == null && next.lastOutboundAt == null) {
|
|
43834
|
+
m.delete(key);
|
|
43835
|
+
} else {
|
|
43836
|
+
m.set(key, next);
|
|
43837
|
+
}
|
|
43838
|
+
return { ...state3, perKey: m };
|
|
43839
|
+
}
|
|
43840
|
+
function chatIdOfKey(key) {
|
|
43841
|
+
const idx = key.indexOf(":");
|
|
43842
|
+
return idx === -1 ? key : key.slice(0, idx);
|
|
43843
|
+
}
|
|
43844
|
+
function sweepSiblings(state3, chatId, exceptKey) {
|
|
43845
|
+
const effects = [];
|
|
43846
|
+
let next = state3;
|
|
43847
|
+
for (const [k, v] of state3.perKey) {
|
|
43848
|
+
if (k === exceptKey)
|
|
43849
|
+
continue;
|
|
43850
|
+
if (chatIdOfKey(k) !== chatId)
|
|
43851
|
+
continue;
|
|
43852
|
+
if (v.turnStartedAt == null)
|
|
43853
|
+
continue;
|
|
43854
|
+
effects.push({ kind: "clearTurnStarted", key: k });
|
|
43855
|
+
next = updatePerKey(next, k, (p) => ({ ...p, turnStartedAt: null }));
|
|
43856
|
+
}
|
|
43857
|
+
return { state: next, effects };
|
|
43858
|
+
}
|
|
43859
|
+
function transition(state3, event) {
|
|
43860
|
+
switch (event.kind) {
|
|
43861
|
+
case "bridgeUp": {
|
|
43862
|
+
if (state3.global.kind !== "bridge_dead") {
|
|
43863
|
+
return { state: state3, effects: [{ kind: "logTrace", stage: "bridgeUp_redundant" }] };
|
|
43864
|
+
}
|
|
43865
|
+
return {
|
|
43866
|
+
state: { ...state3, global: { kind: "bridge_alive_idle" } },
|
|
43867
|
+
effects: [
|
|
43868
|
+
{ kind: "redeliverPersistedPermVerdicts" },
|
|
43869
|
+
{ kind: "drainBuffer" },
|
|
43870
|
+
{ kind: "logTrace", stage: "bridge_recover" }
|
|
43871
|
+
]
|
|
43872
|
+
};
|
|
43873
|
+
}
|
|
43874
|
+
case "bridgeDown": {
|
|
43875
|
+
return {
|
|
43876
|
+
state: { ...state3, global: { kind: "bridge_dead" } },
|
|
43877
|
+
effects: [{ kind: "logTrace", stage: "bridge_flap" }]
|
|
43878
|
+
};
|
|
43879
|
+
}
|
|
43880
|
+
case "inbound": {
|
|
43881
|
+
const isSteering = event.msg.isSteering;
|
|
43882
|
+
const inTurn = state3.global.kind === "bridge_alive_in_turn";
|
|
43883
|
+
const alive = state3.global.kind !== "bridge_dead";
|
|
43884
|
+
if (!alive) {
|
|
43885
|
+
return {
|
|
43886
|
+
state: state3,
|
|
43887
|
+
effects: [
|
|
43888
|
+
{ kind: "bufferInbound", key: event.key, msg: event.msg },
|
|
43889
|
+
{ kind: "persistInbound", key: event.key, msg: event.msg },
|
|
43890
|
+
{ kind: "logTrace", stage: "inbound_bridge_dead_buffer", key: event.key }
|
|
43891
|
+
]
|
|
43892
|
+
};
|
|
43893
|
+
}
|
|
43894
|
+
if (inTurn && !isSteering) {
|
|
43895
|
+
return {
|
|
43896
|
+
state: state3,
|
|
43897
|
+
effects: [
|
|
43898
|
+
{ kind: "bufferInbound", key: event.key, msg: event.msg },
|
|
43899
|
+
{ kind: "persistInbound", key: event.key, msg: event.msg },
|
|
43900
|
+
{ kind: "logTrace", stage: "inbound_held_mid_turn", key: event.key, metadata: { msgId: event.msg.msgId } }
|
|
43901
|
+
]
|
|
43902
|
+
};
|
|
43903
|
+
}
|
|
43904
|
+
if (isSteering) {
|
|
43905
|
+
return {
|
|
43906
|
+
state: state3,
|
|
43907
|
+
effects: [
|
|
43908
|
+
{ kind: "deliverToBridge", key: event.key, msg: event.msg },
|
|
43909
|
+
{ kind: "logTrace", stage: "steer_delivered_mid_turn", key: event.key }
|
|
43910
|
+
]
|
|
43911
|
+
};
|
|
43912
|
+
}
|
|
43913
|
+
const next = {
|
|
43914
|
+
global: { kind: "bridge_alive_in_turn", activeTurn: event.key },
|
|
43915
|
+
perKey: state3.perKey
|
|
43916
|
+
};
|
|
43917
|
+
return {
|
|
43918
|
+
state: updatePerKey(next, event.key, (p) => ({ ...p, turnStartedAt: event.at })),
|
|
43919
|
+
effects: [
|
|
43920
|
+
{ kind: "setTurnStarted", key: event.key, at: event.at },
|
|
43921
|
+
{ kind: "deliverToBridge", key: event.key, msg: event.msg },
|
|
43922
|
+
{ kind: "logTrace", stage: "fresh_turn_deliver", key: event.key, metadata: { msgId: event.msg.msgId } }
|
|
43923
|
+
]
|
|
43924
|
+
};
|
|
43925
|
+
}
|
|
43926
|
+
case "turnStart": {
|
|
43927
|
+
const next = state3.global.kind === "bridge_alive_in_turn" ? state3 : { ...state3, global: { kind: "bridge_alive_in_turn", activeTurn: event.key } };
|
|
43928
|
+
return {
|
|
43929
|
+
state: updatePerKey(next, event.key, (p) => ({ ...p, turnStartedAt: event.at })),
|
|
43930
|
+
effects: [
|
|
43931
|
+
{ kind: "setTurnStarted", key: event.key, at: event.at },
|
|
43932
|
+
{ kind: "logTrace", stage: "turn_start", key: event.key }
|
|
43933
|
+
]
|
|
43934
|
+
};
|
|
43935
|
+
}
|
|
43936
|
+
case "turnEnd": {
|
|
43937
|
+
const chatId = chatIdOfKey(event.key);
|
|
43938
|
+
const stateAfterClear = updatePerKey(state3, event.key, (p) => ({
|
|
43939
|
+
turnStartedAt: null,
|
|
43940
|
+
lastOutboundAt: event.outboundEmitted ? event.at : p.lastOutboundAt
|
|
43941
|
+
}));
|
|
43942
|
+
const sweep = sweepSiblings(stateAfterClear, chatId, event.key);
|
|
43943
|
+
const wasActive = state3.global.kind === "bridge_alive_in_turn" && state3.global.activeTurn === event.key;
|
|
43944
|
+
const next = wasActive ? { ...sweep.state, global: { kind: "bridge_alive_idle" } } : sweep.state;
|
|
43945
|
+
const effects = [
|
|
43946
|
+
{ kind: "clearTurnStarted", key: event.key },
|
|
43947
|
+
...sweep.effects
|
|
43948
|
+
];
|
|
43949
|
+
if (event.outboundEmitted) {
|
|
43950
|
+
effects.push({ kind: "noteOutbound", key: event.key, at: event.at });
|
|
43951
|
+
}
|
|
43952
|
+
effects.push({ kind: "drainBuffer" });
|
|
43953
|
+
effects.push({ kind: "logTrace", stage: "turn_complete", key: event.key, metadata: { outboundEmitted: event.outboundEmitted } });
|
|
43954
|
+
return { state: next, effects };
|
|
43955
|
+
}
|
|
43956
|
+
case "modelOutbound": {
|
|
43957
|
+
return {
|
|
43958
|
+
state: updatePerKey(state3, event.key, (p) => ({ ...p, lastOutboundAt: event.at })),
|
|
43959
|
+
effects: [
|
|
43960
|
+
{ kind: "noteOutbound", key: event.key, at: event.at }
|
|
43961
|
+
]
|
|
43962
|
+
};
|
|
43963
|
+
}
|
|
43964
|
+
case "permVerdict": {
|
|
43965
|
+
const alive = state3.global.kind !== "bridge_dead";
|
|
43966
|
+
if (alive) {
|
|
43967
|
+
return {
|
|
43968
|
+
state: state3,
|
|
43969
|
+
effects: [
|
|
43970
|
+
{ kind: "deliverPermVerdict", verdict: event.verdict },
|
|
43971
|
+
{ kind: "logTrace", stage: "perm_verdict_delivered" }
|
|
43972
|
+
]
|
|
43973
|
+
};
|
|
43974
|
+
}
|
|
43975
|
+
return {
|
|
43976
|
+
state: state3,
|
|
43977
|
+
effects: [
|
|
43978
|
+
{ kind: "persistPermVerdict", verdict: event.verdict },
|
|
43979
|
+
{ kind: "logTrace", stage: "perm_verdict_persisted" }
|
|
43980
|
+
]
|
|
43981
|
+
};
|
|
43982
|
+
}
|
|
43983
|
+
case "tick": {
|
|
43984
|
+
const effects = [];
|
|
43985
|
+
let next = state3;
|
|
43986
|
+
for (const [k, v] of state3.perKey) {
|
|
43987
|
+
if (v.turnStartedAt == null)
|
|
43988
|
+
continue;
|
|
43989
|
+
const age = event.now - v.turnStartedAt;
|
|
43990
|
+
if (age <= TURN_TTL_MS) {
|
|
43991
|
+
continue;
|
|
43992
|
+
}
|
|
43993
|
+
const recentOutbound = v.lastOutboundAt != null && event.now - v.lastOutboundAt < OUTBOUND_RECENT_MS;
|
|
43994
|
+
if (recentOutbound) {
|
|
43995
|
+
effects.push({ kind: "logTrace", stage: "fallback_suppressed", key: k, metadata: { recentOutboundMs: event.now - (v.lastOutboundAt ?? 0) } });
|
|
43996
|
+
continue;
|
|
43997
|
+
}
|
|
43998
|
+
effects.push({ kind: "firePoke", key: k, level: "fallback" });
|
|
43999
|
+
effects.push({ kind: "clearTurnStarted", key: k });
|
|
44000
|
+
next = updatePerKey(next, k, (p) => ({ ...p, turnStartedAt: null }));
|
|
44001
|
+
if (next.global.kind === "bridge_alive_in_turn" && next.global.activeTurn === k) {
|
|
44002
|
+
next = { ...next, global: { kind: "bridge_alive_idle" } };
|
|
44003
|
+
}
|
|
44004
|
+
}
|
|
44005
|
+
return { state: next, effects };
|
|
44006
|
+
}
|
|
44007
|
+
}
|
|
44008
|
+
}
|
|
44009
|
+
|
|
44010
|
+
// gateway/inbound-delivery-machine-shadow.ts
|
|
44011
|
+
var state3 = initialState();
|
|
44012
|
+
var enabled2 = process.env.SWITCHROOM_DELIVERY_MACHINE_SHADOW !== "0";
|
|
44013
|
+
function shadowEmit(event) {
|
|
44014
|
+
if (!enabled2)
|
|
44015
|
+
return [];
|
|
44016
|
+
try {
|
|
44017
|
+
const result = transition(state3, event);
|
|
44018
|
+
state3 = result.state;
|
|
44019
|
+
const effectKinds = result.effects.map((e) => e.kind).join(",");
|
|
44020
|
+
const eventDetail = formatEventDetail(event);
|
|
44021
|
+
process.stderr.write(`gw-trace shadow event=${event.kind}${eventDetail} effects=[${effectKinds}] global=${state3.global.kind} perKeySize=${state3.perKey.size}
|
|
44022
|
+
`);
|
|
44023
|
+
return result.effects;
|
|
44024
|
+
} catch (err) {
|
|
44025
|
+
process.stderr.write(`gw-trace shadow ERROR event=${event.kind} err=${err instanceof Error ? err.message : String(err)}
|
|
44026
|
+
`);
|
|
44027
|
+
return [];
|
|
44028
|
+
}
|
|
44029
|
+
}
|
|
44030
|
+
function formatEventDetail(event) {
|
|
44031
|
+
switch (event.kind) {
|
|
44032
|
+
case "inbound":
|
|
44033
|
+
return ` key=${event.key} msg=${event.msg.msgId} steer=${event.msg.isSteering}`;
|
|
44034
|
+
case "turnStart":
|
|
44035
|
+
case "turnEnd":
|
|
44036
|
+
return ` key=${event.key}${event.kind === "turnEnd" ? ` outbound=${event.outboundEmitted}` : ""}`;
|
|
44037
|
+
case "permVerdict":
|
|
44038
|
+
return ` req=${event.verdict.requestId} behavior=${event.verdict.behavior}`;
|
|
44039
|
+
case "modelOutbound":
|
|
44040
|
+
return ` key=${event.key}`;
|
|
44041
|
+
case "bridgeUp":
|
|
44042
|
+
case "bridgeDown":
|
|
44043
|
+
case "tick":
|
|
44044
|
+
return "";
|
|
44045
|
+
}
|
|
44046
|
+
}
|
|
44047
|
+
|
|
43817
44048
|
// gateway/vault-grant-inbound-builders.ts
|
|
43818
44049
|
function buildVaultGrantApprovedInbound(opts) {
|
|
43819
44050
|
const ts = opts.nowMs ?? Date.now();
|
|
@@ -45248,7 +45479,7 @@ function extractToolResultErrorText(content) {
|
|
|
45248
45479
|
}
|
|
45249
45480
|
return "";
|
|
45250
45481
|
}
|
|
45251
|
-
function projectSubagentLine(line, agentId,
|
|
45482
|
+
function projectSubagentLine(line, agentId, state4) {
|
|
45252
45483
|
let obj;
|
|
45253
45484
|
try {
|
|
45254
45485
|
obj = JSON.parse(line);
|
|
@@ -45261,8 +45492,8 @@ function projectSubagentLine(line, agentId, state3) {
|
|
|
45261
45492
|
if (type === "user") {
|
|
45262
45493
|
const message = obj.message;
|
|
45263
45494
|
const content = message?.content;
|
|
45264
|
-
if (!
|
|
45265
|
-
|
|
45495
|
+
if (!state4.hasEmittedStart) {
|
|
45496
|
+
state4.hasEmittedStart = true;
|
|
45266
45497
|
let promptText = "";
|
|
45267
45498
|
if (typeof content === "string") {
|
|
45268
45499
|
promptText = content;
|
|
@@ -47046,10 +47277,10 @@ function loadCreditState(stateDir) {
|
|
|
47046
47277
|
} catch {}
|
|
47047
47278
|
return emptyCreditState();
|
|
47048
47279
|
}
|
|
47049
|
-
function saveCreditState(stateDir,
|
|
47280
|
+
function saveCreditState(stateDir, state4) {
|
|
47050
47281
|
mkdirSync15(stateDir, { recursive: true });
|
|
47051
47282
|
const path = join27(stateDir, STATE_FILE);
|
|
47052
|
-
writeFileSync17(path, JSON.stringify(
|
|
47283
|
+
writeFileSync17(path, JSON.stringify(state4, null, 2) + `
|
|
47053
47284
|
`, { mode: 384 });
|
|
47054
47285
|
}
|
|
47055
47286
|
|
|
@@ -47126,11 +47357,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
|
|
|
47126
47357
|
}
|
|
47127
47358
|
|
|
47128
47359
|
// ../src/build-info.ts
|
|
47129
|
-
var VERSION = "0.12.
|
|
47130
|
-
var COMMIT_SHA = "
|
|
47131
|
-
var COMMIT_DATE = "2026-05-
|
|
47132
|
-
var LATEST_PR =
|
|
47133
|
-
var COMMITS_AHEAD_OF_TAG =
|
|
47360
|
+
var VERSION = "0.12.25";
|
|
47361
|
+
var COMMIT_SHA = "4b8ab51";
|
|
47362
|
+
var COMMIT_DATE = "2026-05-20T15:24:02+10:00";
|
|
47363
|
+
var LATEST_PR = 1584;
|
|
47364
|
+
var COMMITS_AHEAD_OF_TAG = 2;
|
|
47134
47365
|
|
|
47135
47366
|
// gateway/boot-version.ts
|
|
47136
47367
|
function formatRelativeAgo(iso) {
|
|
@@ -48079,6 +48310,7 @@ function streamKey3(chatId, threadId) {
|
|
|
48079
48310
|
return chatKey(chatId, threadId);
|
|
48080
48311
|
}
|
|
48081
48312
|
function purgeReactionTracking(key) {
|
|
48313
|
+
shadowEmit({ kind: "turnEnd", key, at: Date.now(), outboundEmitted: true });
|
|
48082
48314
|
const msgInfo = activeReactionMsgIds.get(key);
|
|
48083
48315
|
activeStatusReactions.delete(key);
|
|
48084
48316
|
activeReactionMsgIds.delete(key);
|
|
@@ -49002,6 +49234,9 @@ var ipcServer = createIpcServer({
|
|
|
49002
49234
|
onClientRegistered(client3) {
|
|
49003
49235
|
process.stderr.write(`telegram gateway: bridge registered \u2014 agent=${client3.agentName}
|
|
49004
49236
|
`);
|
|
49237
|
+
if (client3.agentName != null) {
|
|
49238
|
+
shadowEmit({ kind: "bridgeUp", at: Date.now() });
|
|
49239
|
+
}
|
|
49005
49240
|
client3.send({ type: "status", status: "agent_connected" });
|
|
49006
49241
|
if (client3.agentName != null) {
|
|
49007
49242
|
const pending = pendingInboundBuffer.drain(client3.agentName);
|
|
@@ -49095,6 +49330,9 @@ var ipcServer = createIpcServer({
|
|
|
49095
49330
|
onClientDisconnected(client3) {
|
|
49096
49331
|
process.stderr.write(`telegram gateway: bridge disconnected \u2014 agent=${client3.agentName}
|
|
49097
49332
|
`);
|
|
49333
|
+
if (client3.agentName != null) {
|
|
49334
|
+
shadowEmit({ kind: "bridgeDown", at: Date.now() });
|
|
49335
|
+
}
|
|
49098
49336
|
flushOnAgentDisconnect({
|
|
49099
49337
|
agentName: client3.agentName,
|
|
49100
49338
|
activeStatusReactions,
|
|
@@ -51057,7 +51295,7 @@ function handleSessionEvent(ev) {
|
|
|
51057
51295
|
}
|
|
51058
51296
|
function handlePtyPartial(text) {
|
|
51059
51297
|
const turn = currentTurn;
|
|
51060
|
-
const
|
|
51298
|
+
const state4 = {
|
|
51061
51299
|
currentSessionChatId: turn?.sessionChatId ?? null,
|
|
51062
51300
|
currentSessionThreadId: turn?.sessionThreadId,
|
|
51063
51301
|
pendingPtyPartial: pendingPtyPartial != null ? { text: pendingPtyPartial } : null,
|
|
@@ -51066,7 +51304,7 @@ function handlePtyPartial(text) {
|
|
|
51066
51304
|
suppressPtyPreview,
|
|
51067
51305
|
lastPtyPreviewByChat
|
|
51068
51306
|
};
|
|
51069
|
-
handlePtyPartialPure(text,
|
|
51307
|
+
handlePtyPartialPure(text, state4, {
|
|
51070
51308
|
bot,
|
|
51071
51309
|
retry: robustApiCall,
|
|
51072
51310
|
renderText: markdownToHtml,
|
|
@@ -51082,7 +51320,7 @@ function handlePtyPartial(text) {
|
|
|
51082
51320
|
},
|
|
51083
51321
|
writeError: (line) => process.stderr.write(line)
|
|
51084
51322
|
});
|
|
51085
|
-
pendingPtyPartial =
|
|
51323
|
+
pendingPtyPartial = state4.pendingPtyPartial?.text ?? null;
|
|
51086
51324
|
}
|
|
51087
51325
|
var gateDenyLastLoggedAt = new Map;
|
|
51088
51326
|
var GATE_DENY_LOG_WINDOW_MS = 60000;
|
|
@@ -51275,6 +51513,17 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
|
|
|
51275
51513
|
return;
|
|
51276
51514
|
}
|
|
51277
51515
|
const inboundReceivedAt = Date.now();
|
|
51516
|
+
const _shadowKey = statusKey(ctx.chat?.id != null ? String(ctx.chat.id) : "0", ctx.message?.message_thread_id);
|
|
51517
|
+
shadowEmit({
|
|
51518
|
+
kind: "inbound",
|
|
51519
|
+
key: _shadowKey,
|
|
51520
|
+
msg: {
|
|
51521
|
+
msgId: ctx.message?.message_id ?? 0,
|
|
51522
|
+
isSteering: false,
|
|
51523
|
+
payload: null
|
|
51524
|
+
},
|
|
51525
|
+
at: Date.now()
|
|
51526
|
+
});
|
|
51278
51527
|
const turnInFlightAtReceipt = activeTurnStartedAt.size > 0;
|
|
51279
51528
|
const access = result.access;
|
|
51280
51529
|
const from = ctx.from;
|
|
@@ -53140,15 +53389,15 @@ async function doFireFleetAutoFallback(triggerAgent) {
|
|
|
53140
53389
|
`);
|
|
53141
53390
|
return false;
|
|
53142
53391
|
}
|
|
53143
|
-
const
|
|
53144
|
-
const probeResp =
|
|
53145
|
-
const quotas =
|
|
53392
|
+
const state4 = await client3.listState();
|
|
53393
|
+
const probeResp = state4.accounts.length > 0 ? await client3.probeQuota(state4.accounts.map((a) => a.label)).catch(() => ({ results: [] })) : { results: [] };
|
|
53394
|
+
const quotas = state4.accounts.map((a) => {
|
|
53146
53395
|
const hit = probeResp.results.find((r) => r.label === a.label);
|
|
53147
53396
|
return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
|
|
53148
53397
|
});
|
|
53149
53398
|
const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
|
|
53150
53399
|
const outcome = await runFleetAutoFallback({
|
|
53151
|
-
state:
|
|
53400
|
+
state: state4,
|
|
53152
53401
|
quotas,
|
|
53153
53402
|
setActive: (label) => client3.setActive(label),
|
|
53154
53403
|
triggerAgent,
|
|
@@ -53334,9 +53583,9 @@ async function probeQuotaForBootCard(agent, timeoutMs) {
|
|
|
53334
53583
|
const client3 = await getAuthBrokerClient(agent);
|
|
53335
53584
|
if (!client3)
|
|
53336
53585
|
return null;
|
|
53337
|
-
const
|
|
53338
|
-
const entry =
|
|
53339
|
-
const label = entry?.override ?? entry?.account ??
|
|
53586
|
+
const state4 = await client3.listState();
|
|
53587
|
+
const entry = state4.agents.find((a) => a.name === agent);
|
|
53588
|
+
const label = entry?.override ?? entry?.account ?? state4.active;
|
|
53340
53589
|
if (!label)
|
|
53341
53590
|
return null;
|
|
53342
53591
|
const { results } = await client3.probeQuota([label], timeoutMs);
|
|
@@ -53853,34 +54102,34 @@ Which keys for <code>${escapeHtmlForTg(agent)}</code>?
|
|
|
53853
54102
|
startedAt: Date.now()
|
|
53854
54103
|
});
|
|
53855
54104
|
}
|
|
53856
|
-
async function grantWizardStep3(ctx, chatId,
|
|
54105
|
+
async function grantWizardStep3(ctx, chatId, state4) {
|
|
53857
54106
|
const kb = buildGrantDurationKeyboard();
|
|
53858
|
-
const keyList =
|
|
54107
|
+
const keyList = state4.selectedKeys.map((k) => `\u2022 <code>${escapeHtmlForTg(k)}</code>`).join(`
|
|
53859
54108
|
`);
|
|
53860
54109
|
const text = `<b>Grant capability token \u2014 Step 3/3</b>
|
|
53861
54110
|
|
|
53862
|
-
Keys for <code>${escapeHtmlForTg(
|
|
54111
|
+
Keys for <code>${escapeHtmlForTg(state4.agent)}</code>:
|
|
53863
54112
|
${keyList}
|
|
53864
54113
|
|
|
53865
54114
|
How long should this grant be valid?`;
|
|
53866
|
-
const msgId =
|
|
54115
|
+
const msgId = state4.wizardMsgId;
|
|
53867
54116
|
if (msgId != null) {
|
|
53868
54117
|
await ctx.api.editMessageText(chatId, msgId, text, { parse_mode: "HTML", reply_markup: kb }).catch(() => {});
|
|
53869
54118
|
} else {
|
|
53870
54119
|
const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb });
|
|
53871
|
-
|
|
54120
|
+
state4.wizardMsgId = sent?.message_id;
|
|
53872
54121
|
}
|
|
53873
|
-
pendingVaultOps.set(chatId, { ...
|
|
54122
|
+
pendingVaultOps.set(chatId, { ...state4, step: "duration" });
|
|
53874
54123
|
}
|
|
53875
|
-
async function grantWizardConfirm(ctx, chatId,
|
|
54124
|
+
async function grantWizardConfirm(ctx, chatId, state4) {
|
|
53876
54125
|
const kb = buildGrantConfirmKeyboard();
|
|
53877
|
-
const expiresLabel = formatGrantExpiry(
|
|
53878
|
-
const keyList =
|
|
54126
|
+
const expiresLabel = formatGrantExpiry(state4.ttlSeconds);
|
|
54127
|
+
const keyList = state4.selectedKeys.map((k) => `\u2022 <code>${escapeHtmlForTg(k)}</code>`).join(`
|
|
53879
54128
|
`);
|
|
53880
54129
|
const text = [
|
|
53881
54130
|
"<b>Confirm grant</b>",
|
|
53882
54131
|
"",
|
|
53883
|
-
`Agent: <code>${escapeHtmlForTg(
|
|
54132
|
+
`Agent: <code>${escapeHtmlForTg(state4.agent)}</code>`,
|
|
53884
54133
|
`Keys:
|
|
53885
54134
|
${keyList}`,
|
|
53886
54135
|
`Expires: <b>${escapeHtmlForTg(expiresLabel)}</b>`,
|
|
@@ -53888,34 +54137,34 @@ ${keyList}`,
|
|
|
53888
54137
|
"Tap <b>Generate</b> to mint the token."
|
|
53889
54138
|
].join(`
|
|
53890
54139
|
`);
|
|
53891
|
-
const msgId =
|
|
54140
|
+
const msgId = state4.wizardMsgId;
|
|
53892
54141
|
if (msgId != null) {
|
|
53893
54142
|
await ctx.api.editMessageText(chatId, msgId, text, { parse_mode: "HTML", reply_markup: kb }).catch(() => {});
|
|
53894
54143
|
} else {
|
|
53895
54144
|
const sent = await switchroomReply(ctx, text, { html: true, reply_markup: kb });
|
|
53896
|
-
|
|
54145
|
+
state4.wizardMsgId = sent?.message_id;
|
|
53897
54146
|
}
|
|
53898
|
-
const kernelRequestId = await mintGrantWizardKernelRequest(
|
|
54147
|
+
const kernelRequestId = await mintGrantWizardKernelRequest(state4.agent, loadAccess().allowFrom, state4.selectedKeys, state4.ttlSeconds ?? null);
|
|
53899
54148
|
pendingVaultOps.set(chatId, {
|
|
53900
|
-
...
|
|
54149
|
+
...state4,
|
|
53901
54150
|
step: "confirm",
|
|
53902
54151
|
expiresLabel,
|
|
53903
|
-
kernel_request_id: kernelRequestId ??
|
|
54152
|
+
kernel_request_id: kernelRequestId ?? state4.kernel_request_id
|
|
53904
54153
|
});
|
|
53905
54154
|
}
|
|
53906
|
-
async function executeGrantWizard(ctx, chatId,
|
|
54155
|
+
async function executeGrantWizard(ctx, chatId, state4) {
|
|
53907
54156
|
pendingVaultOps.delete(chatId);
|
|
53908
|
-
await recordGrantWizardKernelDecision(
|
|
54157
|
+
await recordGrantWizardKernelDecision(state4.kernel_request_id, "allow_once", ctx.from?.id ?? 0, loadAccess().allowFrom);
|
|
53909
54158
|
try {
|
|
53910
|
-
assertSafeAgentName(
|
|
54159
|
+
assertSafeAgentName(state4.agent);
|
|
53911
54160
|
} catch {
|
|
53912
54161
|
return;
|
|
53913
54162
|
}
|
|
53914
54163
|
const result = await mintGrantViaBroker({
|
|
53915
|
-
agent:
|
|
53916
|
-
keys:
|
|
53917
|
-
ttl_seconds:
|
|
53918
|
-
description:
|
|
54164
|
+
agent: state4.agent,
|
|
54165
|
+
keys: state4.selectedKeys,
|
|
54166
|
+
ttl_seconds: state4.ttlSeconds ?? null,
|
|
54167
|
+
description: state4.description
|
|
53919
54168
|
});
|
|
53920
54169
|
if (result.kind === "unreachable") {
|
|
53921
54170
|
await switchroomReply(ctx, `\uD83D\uDD34 Broker unreachable: ${escapeHtmlForTg(result.msg)}`, { html: true });
|
|
@@ -53926,16 +54175,16 @@ async function executeGrantWizard(ctx, chatId, state3) {
|
|
|
53926
54175
|
return;
|
|
53927
54176
|
}
|
|
53928
54177
|
const { token, id } = result;
|
|
53929
|
-
const tokenPath = join32(homedir12(), ".switchroom", "agents",
|
|
54178
|
+
const tokenPath = join32(homedir12(), ".switchroom", "agents", state4.agent, ".vault-token");
|
|
53930
54179
|
try {
|
|
53931
|
-
mkdirSync21(join32(homedir12(), ".switchroom", "agents",
|
|
54180
|
+
mkdirSync21(join32(homedir12(), ".switchroom", "agents", state4.agent), { recursive: true });
|
|
53932
54181
|
writeFileSync21(tokenPath, token, { mode: 384 });
|
|
53933
54182
|
} catch (err) {
|
|
53934
54183
|
await switchroomReply(ctx, `<b>Grant created but token write failed:</b> ${escapeHtmlForTg(String(err))}`, { html: true });
|
|
53935
54184
|
return;
|
|
53936
54185
|
}
|
|
53937
|
-
const msgId =
|
|
53938
|
-
const successText = `\u2705 Grant <code>${escapeHtmlForTg(id)}</code> created. Written to <code>~/.switchroom/agents/${escapeHtmlForTg(
|
|
54186
|
+
const msgId = state4.wizardMsgId;
|
|
54187
|
+
const successText = `\u2705 Grant <code>${escapeHtmlForTg(id)}</code> created. Written to <code>~/.switchroom/agents/${escapeHtmlForTg(state4.agent)}/.vault-token</code>`;
|
|
53939
54188
|
if (msgId != null) {
|
|
53940
54189
|
await finalizeCallback(ctx, {
|
|
53941
54190
|
ackText: "\u2705 Grant created",
|
|
@@ -54029,59 +54278,59 @@ Keys: <code>${escapeHtmlForTg(grant.key_allow.join(", "))}</code>`;
|
|
|
54029
54278
|
await ackSilently();
|
|
54030
54279
|
return;
|
|
54031
54280
|
}
|
|
54032
|
-
const
|
|
54033
|
-
if (!
|
|
54281
|
+
const state4 = pendingVaultOps.get(chatId);
|
|
54282
|
+
if (!state4 || state4.kind !== "grant-wizard") {
|
|
54034
54283
|
await ctx.editMessageText("\u26A0\uFE0F Wizard session expired. Run /vault grant to start again.").catch(() => {});
|
|
54035
54284
|
await ackSilently();
|
|
54036
54285
|
return;
|
|
54037
54286
|
}
|
|
54038
54287
|
if (data.startsWith("vg:agent:")) {
|
|
54039
54288
|
const agent = data.slice("vg:agent:".length);
|
|
54040
|
-
const msgId = ctx.callbackQuery?.message?.message_id ??
|
|
54289
|
+
const msgId = ctx.callbackQuery?.message?.message_id ?? state4.wizardMsgId;
|
|
54041
54290
|
await grantWizardStep2(ctx, chatId, agent, msgId);
|
|
54042
54291
|
await ackSilently();
|
|
54043
54292
|
return;
|
|
54044
54293
|
}
|
|
54045
54294
|
if (data.startsWith("vg:key:")) {
|
|
54046
54295
|
const key = data.slice("vg:key:".length);
|
|
54047
|
-
if (
|
|
54296
|
+
if (state4.step !== "keys") {
|
|
54048
54297
|
await ackSilently();
|
|
54049
54298
|
return;
|
|
54050
54299
|
}
|
|
54051
|
-
const selectedSet = new Set(
|
|
54300
|
+
const selectedSet = new Set(state4.selectedKeys ?? []);
|
|
54052
54301
|
if (selectedSet.has(key)) {
|
|
54053
54302
|
selectedSet.delete(key);
|
|
54054
54303
|
} else {
|
|
54055
54304
|
selectedSet.add(key);
|
|
54056
54305
|
}
|
|
54057
|
-
const updatedState = { ...
|
|
54306
|
+
const updatedState = { ...state4, selectedKeys: [...selectedSet] };
|
|
54058
54307
|
pendingVaultOps.set(chatId, updatedState);
|
|
54059
|
-
const kb = buildGrantKeysKeyboard(
|
|
54308
|
+
const kb = buildGrantKeysKeyboard(state4.availableKeys ?? [], selectedSet);
|
|
54060
54309
|
await ctx.editMessageReplyMarkup({ reply_markup: kb }).catch(() => {});
|
|
54061
54310
|
await ackSilently();
|
|
54062
54311
|
return;
|
|
54063
54312
|
}
|
|
54064
54313
|
if (data === "vg:keys-continue") {
|
|
54065
|
-
if (
|
|
54314
|
+
if (state4.step !== "keys") {
|
|
54066
54315
|
await ackSilently();
|
|
54067
54316
|
return;
|
|
54068
54317
|
}
|
|
54069
|
-
if (!
|
|
54318
|
+
if (!state4.selectedKeys || state4.selectedKeys.length === 0) {
|
|
54070
54319
|
await ctx.answerCallbackQuery({ text: "Select at least one key." }).catch(() => {});
|
|
54071
54320
|
return;
|
|
54072
54321
|
}
|
|
54073
|
-
await grantWizardStep3(ctx, chatId,
|
|
54322
|
+
await grantWizardStep3(ctx, chatId, state4);
|
|
54074
54323
|
await ackSilently();
|
|
54075
54324
|
return;
|
|
54076
54325
|
}
|
|
54077
54326
|
if (data.startsWith("vg:dur:")) {
|
|
54078
|
-
if (
|
|
54327
|
+
if (state4.step !== "duration") {
|
|
54079
54328
|
await ackSilently();
|
|
54080
54329
|
return;
|
|
54081
54330
|
}
|
|
54082
54331
|
const dur = data.slice("vg:dur:".length);
|
|
54083
54332
|
if (dur === "custom") {
|
|
54084
|
-
pendingVaultOps.set(chatId, { ...
|
|
54333
|
+
pendingVaultOps.set(chatId, { ...state4, awaitingCustomDuration: true });
|
|
54085
54334
|
const msg = ctx.callbackQuery?.message;
|
|
54086
54335
|
if (msg && "text" in msg && msg.text) {
|
|
54087
54336
|
await ctx.editMessageText(escapeHtmlForTg(msg.text) + `
|
|
@@ -54103,27 +54352,27 @@ Keys: <code>${escapeHtmlForTg(grant.key_allow.join(", "))}</code>`;
|
|
|
54103
54352
|
return;
|
|
54104
54353
|
}
|
|
54105
54354
|
}
|
|
54106
|
-
const newState = { ...
|
|
54355
|
+
const newState = { ...state4, ttlSeconds, awaitingCustomDuration: false };
|
|
54107
54356
|
await grantWizardConfirm(ctx, chatId, newState);
|
|
54108
54357
|
await ackSilently();
|
|
54109
54358
|
return;
|
|
54110
54359
|
}
|
|
54111
54360
|
if (data === "vg:back:duration") {
|
|
54112
|
-
if (
|
|
54361
|
+
if (state4.step !== "duration") {
|
|
54113
54362
|
await ackSilently();
|
|
54114
54363
|
return;
|
|
54115
54364
|
}
|
|
54116
|
-
const msgId =
|
|
54117
|
-
await grantWizardStep2(ctx, chatId,
|
|
54365
|
+
const msgId = state4.wizardMsgId;
|
|
54366
|
+
await grantWizardStep2(ctx, chatId, state4.agent, msgId);
|
|
54118
54367
|
await ackSilently();
|
|
54119
54368
|
return;
|
|
54120
54369
|
}
|
|
54121
54370
|
if (data === "vg:generate") {
|
|
54122
|
-
if (
|
|
54371
|
+
if (state4.step !== "confirm") {
|
|
54123
54372
|
await ackSilently();
|
|
54124
54373
|
return;
|
|
54125
54374
|
}
|
|
54126
|
-
await executeGrantWizard(ctx, chatId,
|
|
54375
|
+
await executeGrantWizard(ctx, chatId, state4);
|
|
54127
54376
|
await ackSilently();
|
|
54128
54377
|
return;
|
|
54129
54378
|
}
|
|
@@ -54361,15 +54610,15 @@ async function handleAuthDashboardCallback(ctx) {
|
|
|
54361
54610
|
} catch {}
|
|
54362
54611
|
return;
|
|
54363
54612
|
}
|
|
54364
|
-
const
|
|
54365
|
-
const probeResp =
|
|
54366
|
-
const quotas =
|
|
54613
|
+
const state4 = await client3.listState();
|
|
54614
|
+
const probeResp = state4.accounts.length > 0 ? await client3.probeQuota(state4.accounts.map((a) => a.label)).catch(() => ({ results: [] })) : { results: [] };
|
|
54615
|
+
const quotas = state4.accounts.map((a) => {
|
|
54367
54616
|
const hit = probeResp.results.find((r) => r.label === a.label);
|
|
54368
54617
|
return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
|
|
54369
54618
|
});
|
|
54370
54619
|
const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
|
|
54371
54620
|
const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState3, buildSnapshotKeyboard: buildSnapshotKeyboard3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
|
|
54372
|
-
const snapshots = buildSnapshotsFromState3(
|
|
54621
|
+
const snapshots = buildSnapshotsFromState3(state4, quotas);
|
|
54373
54622
|
const text = renderAuthSnapshotFormat23(snapshots, {
|
|
54374
54623
|
tz,
|
|
54375
54624
|
now: new Date,
|
|
@@ -54757,16 +55006,16 @@ bot.command("usage", async (ctx) => {
|
|
|
54757
55006
|
try {
|
|
54758
55007
|
const client3 = await getAuthBrokerClient(currentAgent);
|
|
54759
55008
|
if (client3) {
|
|
54760
|
-
const
|
|
54761
|
-
if (
|
|
54762
|
-
const probeResp = await client3.probeQuota(
|
|
54763
|
-
const quotas =
|
|
55009
|
+
const state4 = await client3.listState();
|
|
55010
|
+
if (state4.accounts.length > 0) {
|
|
55011
|
+
const probeResp = await client3.probeQuota(state4.accounts.map((a) => a.label)).catch(() => ({ results: [] }));
|
|
55012
|
+
const quotas = state4.accounts.map((a) => {
|
|
54764
55013
|
const hit = probeResp.results.find((r) => r.label === a.label);
|
|
54765
55014
|
return hit?.result ?? { ok: false, reason: "broker returned no result for account" };
|
|
54766
55015
|
});
|
|
54767
55016
|
const { renderAuthSnapshotFormat2: renderAuthSnapshotFormat23, buildSnapshotsFromState: buildSnapshotsFromState3 } = await Promise.resolve().then(() => (init_auth_snapshot_format(), exports_auth_snapshot_format));
|
|
54768
55017
|
const tz = process.env.SWITCHROOM_TIMEZONE ?? process.env.TZ ?? "UTC";
|
|
54769
|
-
const snapshots = buildSnapshotsFromState3(
|
|
55018
|
+
const snapshots = buildSnapshotsFromState3(state4, quotas);
|
|
54770
55019
|
const text = renderAuthSnapshotFormat23(snapshots, {
|
|
54771
55020
|
tz,
|
|
54772
55021
|
now: new Date,
|
|
@@ -55606,7 +55855,7 @@ function handleChecklistUpdate(ctx, kind) {
|
|
|
55606
55855
|
const taskId = task.id != null ? String(task.id) : "?";
|
|
55607
55856
|
const user = task.user;
|
|
55608
55857
|
const userName = user?.username ?? (user?.id != null ? String(user.id) : "unknown");
|
|
55609
|
-
const
|
|
55858
|
+
const state4 = kind === "checklist_tasks_done" ? task.done === false ? "undone" : "done" : "added";
|
|
55610
55859
|
const inboundMsg = {
|
|
55611
55860
|
type: "inbound",
|
|
55612
55861
|
chatId: chat_id,
|
|
@@ -55614,20 +55863,20 @@ function handleChecklistUpdate(ctx, kind) {
|
|
|
55614
55863
|
user: userName,
|
|
55615
55864
|
userId: user?.id ?? 0,
|
|
55616
55865
|
ts,
|
|
55617
|
-
text: `(checklist task ${
|
|
55866
|
+
text: `(checklist task ${state4}: id=${taskId})`,
|
|
55618
55867
|
meta: {
|
|
55619
55868
|
chat_id,
|
|
55620
55869
|
message_id,
|
|
55621
55870
|
kind: "checklist_task_changed",
|
|
55622
55871
|
task_id: taskId,
|
|
55623
|
-
state:
|
|
55872
|
+
state: state4,
|
|
55624
55873
|
user: userName,
|
|
55625
55874
|
user_id: user?.id != null ? String(user.id) : "0",
|
|
55626
55875
|
ts: new Date(ts * 1000).toISOString()
|
|
55627
55876
|
}
|
|
55628
55877
|
};
|
|
55629
55878
|
ipcServer.broadcast(inboundMsg);
|
|
55630
|
-
process.stderr.write(`telegram gateway: checklist ${kind}: chat_id=${chat_id} message_id=${message_id} task_id=${taskId} state=${
|
|
55879
|
+
process.stderr.write(`telegram gateway: checklist ${kind}: chat_id=${chat_id} message_id=${message_id} task_id=${taskId} state=${state4} user=${userName}
|
|
55631
55880
|
`);
|
|
55632
55881
|
}
|
|
55633
55882
|
} catch (err) {
|
|
@@ -254,6 +254,13 @@ import { purgeStaleTurnsForChat } from './turn-state-purge.js'
|
|
|
254
254
|
import { decideInboundDelivery } from './inbound-delivery-gate.js'
|
|
255
255
|
import { createPendingPermissionBuffer } from './pending-permission-decisions.js'
|
|
256
256
|
import { chatKey, chatKeyWithSuffix } from './chat-key.js'
|
|
257
|
+
// Phase 2b PR 2 — shadow mode. Each event-site below calls shadowEmit()
|
|
258
|
+
// to record what the InboundDeliveryStateMachine PREDICTS the gateway
|
|
259
|
+
// should do. Behavior unchanged in this PR — the imperative code below
|
|
260
|
+
// still runs everything. PR 3 will cut over to executing the machine's
|
|
261
|
+
// effects.
|
|
262
|
+
import { shadowEmit } from './inbound-delivery-machine-shadow.js'
|
|
263
|
+
import type { ChatKey as _ChatKey } from './inbound-delivery-machine.js'
|
|
257
264
|
import {
|
|
258
265
|
buildVaultGrantApprovedInbound,
|
|
259
266
|
buildVaultGrantDeniedInbound,
|
|
@@ -1265,6 +1272,13 @@ function streamKey(chatId: string, threadId?: number | null): string {
|
|
|
1265
1272
|
}
|
|
1266
1273
|
|
|
1267
1274
|
function purgeReactionTracking(key: string): void {
|
|
1275
|
+
// Phase 2b shadow: turn end. The key was registered via setTurnStarted
|
|
1276
|
+
// when the inbound arrived; purge is the canonical turn-end signal.
|
|
1277
|
+
// outboundEmitted is approximated `true` here — refined in PR 3 to read
|
|
1278
|
+
// from the per-turn `replyCalled` flag on `currentTurn`. Conservative
|
|
1279
|
+
// shadow approximation is safe (only affects machine's lastOutboundAt
|
|
1280
|
+
// tracking; can't drive incorrect behavior in shadow mode).
|
|
1281
|
+
shadowEmit({ kind: 'turnEnd', key: key as _ChatKey, at: Date.now(), outboundEmitted: true })
|
|
1268
1282
|
const msgInfo = activeReactionMsgIds.get(key)
|
|
1269
1283
|
activeStatusReactions.delete(key)
|
|
1270
1284
|
activeReactionMsgIds.delete(key)
|
|
@@ -3182,6 +3196,17 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
3182
3196
|
|
|
3183
3197
|
onClientRegistered(client: IpcClient) {
|
|
3184
3198
|
process.stderr.write(`telegram gateway: bridge registered — agent=${client.agentName}\n`)
|
|
3199
|
+
// Phase 2b shadow: ONLY emit bridgeUp for the REAL bridge sidecar
|
|
3200
|
+
// (with an agent name). Anonymous IPC clients (recall.py, mcp
|
|
3201
|
+
// handshakes, etc.) connect briefly without a name and would
|
|
3202
|
+
// false-positive a bridgeUp/bridgeDown cycle that doesn't reflect
|
|
3203
|
+
// the real bridge state. This bug — discovered post-v0.12.24 — was
|
|
3204
|
+
// causing the shadow state to read `bridge_dead` even when the
|
|
3205
|
+
// real bridge was healthy, because every recall.py connect+disconnect
|
|
3206
|
+
// would flip the state.
|
|
3207
|
+
if (client.agentName != null) {
|
|
3208
|
+
shadowEmit({ kind: 'bridgeUp', at: Date.now() })
|
|
3209
|
+
}
|
|
3185
3210
|
client.send({ type: 'status', status: 'agent_connected' })
|
|
3186
3211
|
|
|
3187
3212
|
// #1150: drain any synthetic inbounds queued for this agent while
|
|
@@ -3307,6 +3332,12 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
3307
3332
|
|
|
3308
3333
|
onClientDisconnected(client: IpcClient) {
|
|
3309
3334
|
process.stderr.write(`telegram gateway: bridge disconnected — agent=${client.agentName}\n`)
|
|
3335
|
+
// Phase 2b shadow: ONLY emit bridgeDown for the REAL bridge sidecar
|
|
3336
|
+
// (matching the bridgeUp gate above). Anonymous IPC clients
|
|
3337
|
+
// disconnect frequently — those are not bridge flaps.
|
|
3338
|
+
if (client.agentName != null) {
|
|
3339
|
+
shadowEmit({ kind: 'bridgeDown', at: Date.now() })
|
|
3340
|
+
}
|
|
3310
3341
|
|
|
3311
3342
|
// Scope the flush to clients that actually registered as an agent.
|
|
3312
3343
|
// Anonymous one-shot connections (e.g. recall.py's legacy
|
|
@@ -6637,6 +6668,23 @@ async function handleInbound(
|
|
|
6637
6668
|
// network RTT) but not a user-perceived end-to-end measurement.
|
|
6638
6669
|
const inboundReceivedAt = Date.now()
|
|
6639
6670
|
|
|
6671
|
+
// Phase 2b shadow: inbound arrival. Emit BEFORE the snapshot/gate
|
|
6672
|
+
// logic so the machine sees the event at the same point in time the
|
|
6673
|
+
// imperative code would. The machine internally handles fresh-turn
|
|
6674
|
+
// vs mid-turn — its decision will be visible in the gw-trace shadow
|
|
6675
|
+
// line emitted to stderr.
|
|
6676
|
+
const _shadowKey = statusKey(ctx.chat?.id != null ? String(ctx.chat.id) : '0', ctx.message?.message_thread_id) as _ChatKey
|
|
6677
|
+
shadowEmit({
|
|
6678
|
+
kind: 'inbound',
|
|
6679
|
+
key: _shadowKey,
|
|
6680
|
+
msg: {
|
|
6681
|
+
msgId: ctx.message?.message_id ?? 0,
|
|
6682
|
+
isSteering: false, // refined in PR 3 — for now shadow conservatively classifies as non-steering
|
|
6683
|
+
payload: null,
|
|
6684
|
+
},
|
|
6685
|
+
at: Date.now(),
|
|
6686
|
+
})
|
|
6687
|
+
|
|
6640
6688
|
// #1556 self-blocking fix (v0.12.22): snapshot the live turn-state
|
|
6641
6689
|
// BEFORE the fresh-turn branch (line ~7357) sets activeTurnStartedAt
|
|
6642
6690
|
// for THIS inbound. The #1556 delivery gate further down asks "is a
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InboundDeliveryStateMachine — SHADOW MODE wiring (Phase 2b PR 2).
|
|
3
|
+
*
|
|
4
|
+
* Per RFC `docs/rfcs/inbound-delivery-state-machine.md` Phase 2b PR 2:
|
|
5
|
+
* the state machine runs ALONGSIDE the existing imperative gateway
|
|
6
|
+
* code, recording predicted effects to a structured trace. Behavior
|
|
7
|
+
* is unchanged — every existing code path still executes the actual
|
|
8
|
+
* I/O. This module's job:
|
|
9
|
+
*
|
|
10
|
+
* 1. Own the module-scope machine state.
|
|
11
|
+
* 2. Expose `shadowEmit(event)` that runs `transition()` + logs the
|
|
12
|
+
* predicted effects via `gw-trace shadow ...` stderr lines.
|
|
13
|
+
* 3. Provide test hooks for resetting + inspecting state.
|
|
14
|
+
*
|
|
15
|
+
* After PR 2 bakes on the fleet for 24+ hours, PR 3 will wire the
|
|
16
|
+
* effects to drive ACTUAL behavior (the cutover), at which point the
|
|
17
|
+
* imperative paths get deleted in PR 4.
|
|
18
|
+
*
|
|
19
|
+
* Telemetry approach: each `shadowEmit` writes a single stderr line
|
|
20
|
+
* with the event kind + emitted effect kinds. Operators can then:
|
|
21
|
+
*
|
|
22
|
+
* docker exec switchroom-<agent> sh -lc 'grep "gw-trace shadow" \
|
|
23
|
+
* /var/log/switchroom/gateway-supervisor.log | tail -50'
|
|
24
|
+
*
|
|
25
|
+
* to see what the state machine PREDICTS the gateway should do for
|
|
26
|
+
* each event. Comparing against the actual log lines (`pending-inbound-
|
|
27
|
+
* buffer: agent=X buffered ...` etc.) is the validation that the
|
|
28
|
+
* machine is bit-identical with reality.
|
|
29
|
+
*
|
|
30
|
+
* Toggle off via `SWITCHROOM_DELIVERY_MACHINE_SHADOW=0` — a kill
|
|
31
|
+
* switch for the case where the shadow emits prove problematic
|
|
32
|
+
* (e.g., trace volume too high). The default is ON.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
type Effect,
|
|
37
|
+
type Event,
|
|
38
|
+
type State,
|
|
39
|
+
initialState,
|
|
40
|
+
transition,
|
|
41
|
+
} from './inbound-delivery-machine.js'
|
|
42
|
+
|
|
43
|
+
let state: State = initialState()
|
|
44
|
+
const enabled = process.env.SWITCHROOM_DELIVERY_MACHINE_SHADOW !== '0'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Run an event through the state machine in shadow mode. The machine
|
|
48
|
+
* state advances, the predicted effects are LOGGED, but no I/O fires.
|
|
49
|
+
*
|
|
50
|
+
* Returns the effects for callers that want to inspect them inline
|
|
51
|
+
* (e.g., the eventual PR 3 cutover will replace the return-and-ignore
|
|
52
|
+
* pattern here with return-and-execute).
|
|
53
|
+
*/
|
|
54
|
+
export function shadowEmit(event: Event): readonly Effect[] {
|
|
55
|
+
if (!enabled) return []
|
|
56
|
+
// Shadow mode MUST NEVER break the gateway. The state machine is
|
|
57
|
+
// pure (no I/O, no async) and property-tested over 5000 schedules,
|
|
58
|
+
// so transition() won't throw on well-formed input. But event
|
|
59
|
+
// construction at the call site could mis-shape inputs; the
|
|
60
|
+
// try/catch is belt-and-braces so a shadow bug never wedges a real
|
|
61
|
+
// turn. The catch logs and bails — the imperative gateway code
|
|
62
|
+
// below the emit point still runs.
|
|
63
|
+
try {
|
|
64
|
+
const result = transition(state, event)
|
|
65
|
+
state = result.state
|
|
66
|
+
|
|
67
|
+
// Single structured stderr line per event — grep-friendly and
|
|
68
|
+
// low volume (one line per gateway event, not per effect). The
|
|
69
|
+
// format matches gateway.ts's existing `tg-post method=...` shape
|
|
70
|
+
// so log aggregation can pick it up without a new parser.
|
|
71
|
+
const effectKinds = result.effects.map((e) => e.kind).join(',')
|
|
72
|
+
const eventDetail = formatEventDetail(event)
|
|
73
|
+
process.stderr.write(
|
|
74
|
+
`gw-trace shadow event=${event.kind}${eventDetail} effects=[${effectKinds}] global=${state.global.kind} perKeySize=${state.perKey.size}\n`,
|
|
75
|
+
)
|
|
76
|
+
return result.effects
|
|
77
|
+
} catch (err) {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
`gw-trace shadow ERROR event=${event.kind} err=${err instanceof Error ? err.message : String(err)}\n`,
|
|
80
|
+
)
|
|
81
|
+
return []
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Compact event detail for the trace line — keeps the line one-line. */
|
|
86
|
+
function formatEventDetail(event: Event): string {
|
|
87
|
+
switch (event.kind) {
|
|
88
|
+
case 'inbound':
|
|
89
|
+
return ` key=${event.key} msg=${event.msg.msgId} steer=${event.msg.isSteering}`
|
|
90
|
+
case 'turnStart':
|
|
91
|
+
case 'turnEnd':
|
|
92
|
+
return ` key=${event.key}${event.kind === 'turnEnd' ? ` outbound=${event.outboundEmitted}` : ''}`
|
|
93
|
+
case 'permVerdict':
|
|
94
|
+
return ` req=${event.verdict.requestId} behavior=${event.verdict.behavior}`
|
|
95
|
+
case 'modelOutbound':
|
|
96
|
+
return ` key=${event.key}`
|
|
97
|
+
case 'bridgeUp':
|
|
98
|
+
case 'bridgeDown':
|
|
99
|
+
case 'tick':
|
|
100
|
+
return ''
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Test hook: reset state to the initial empty machine. */
|
|
105
|
+
export function __shadowResetForTests(): void {
|
|
106
|
+
state = initialState()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Test hook: read the current shadow state. */
|
|
110
|
+
export function __shadowGetStateForTests(): State {
|
|
111
|
+
return state
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Test hook: check if shadow mode is enabled (mirrors the env-var). */
|
|
115
|
+
export function __shadowEnabledForTests(): boolean {
|
|
116
|
+
return enabled
|
|
117
|
+
}
|