switchroom 0.21.1 → 0.21.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/bin/handoff-briefing.sh +23 -1
- package/dist/auth-broker/index.js +54 -34
- package/dist/cli/switchroom.js +840 -379
- package/dist/host-control/main.js +8 -1
- package/package.json +3 -2
- package/telegram-plugin/dist/gateway/gateway.js +378 -111
- package/telegram-plugin/external-spend.ts +1 -1
- package/telegram-plugin/gateway/gateway.ts +3 -1
- package/telegram-plugin/gateway/orphaned-db-sweep.ts +315 -0
- package/telegram-plugin/history.ts +328 -62
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +19 -4
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -2
- package/telegram-plugin/tests/orphaned-db-sweep.test.ts +713 -0
package/bin/handoff-briefing.sh
CHANGED
|
@@ -136,6 +136,7 @@ if [ -n "$TELEGRAM_STATE" ] && [ -d "$TELEGRAM_STATE" ]; then
|
|
|
136
136
|
# under `2>/dev/null`, so the breadcrumb is a debug/manual-run diagnostic.
|
|
137
137
|
TELEGRAM_ROWS=$(python3 - "$HISTORY_DB" "$MAX_MESSAGES" "$TARGET_CHAT_ID" "$TARGET_THREAD_ID" <<'PYEOF'
|
|
138
138
|
import sys, sqlite3, datetime
|
|
139
|
+
from urllib.parse import quote
|
|
139
140
|
|
|
140
141
|
db_path = sys.argv[1]
|
|
141
142
|
limit = int(sys.argv[2])
|
|
@@ -143,7 +144,28 @@ target_chat = sys.argv[3] if len(sys.argv) > 3 else ""
|
|
|
143
144
|
target_thread = sys.argv[4] if len(sys.argv) > 4 else ""
|
|
144
145
|
|
|
145
146
|
try:
|
|
146
|
-
|
|
147
|
+
# READ-ONLY, ALWAYS. `sqlite3.connect(path)` defaults to READ-WRITE, which
|
|
148
|
+
# makes this briefing assembler a writer on the gateway's live WAL DB. A
|
|
149
|
+
# read-write connection that closes as the LAST connection runs SQLite's
|
|
150
|
+
# checkpoint-and-delete path and UNLINKS `history.db-wal` / `-shm` — and
|
|
151
|
+
# this script runs at agent boot, precisely when the gateway is down or
|
|
152
|
+
# starting and this process IS the last connection. Any handle still
|
|
153
|
+
# mapped to those inodes then writes into deleted files: every INSERT
|
|
154
|
+
# reports success and every row is gone at the next restart (the
|
|
155
|
+
# `/proc/<pid>/fd/N -> history.db-wal (deleted)` signature that #4595
|
|
156
|
+
# sweeps for after the fact).
|
|
157
|
+
#
|
|
158
|
+
# `mode=ro` removes that primitive entirely: a read-only connection cannot
|
|
159
|
+
# checkpoint and cannot unlink a sidecar. It still reads a WAL database
|
|
160
|
+
# correctly in every boot state we care about — sidecars present, `-shm`
|
|
161
|
+
# absent, and `-shm` absent with an unwritable directory (SQLite falls back
|
|
162
|
+
# to a heap wal-index rather than failing). Every statement below is a
|
|
163
|
+
# SELECT or a PRAGMA table_info, so nothing here needs write access.
|
|
164
|
+
#
|
|
165
|
+
# The path is percent-encoded: a `?` or `#` in the state dir would
|
|
166
|
+
# otherwise be parsed as the URI's query/fragment delimiter and silently
|
|
167
|
+
# truncate the filename.
|
|
168
|
+
conn = sqlite3.connect("file:" + quote(db_path) + "?mode=ro", uri=True)
|
|
147
169
|
conn.row_factory = sqlite3.Row
|
|
148
170
|
cur = conn.cursor()
|
|
149
171
|
|
|
@@ -20351,6 +20351,8 @@ var EXTERNAL_SPEND_TOP_N = 3;
|
|
|
20351
20351
|
var DEFAULT_LITELLM_BASE = "http://127.0.0.1:4010";
|
|
20352
20352
|
var EXTERNAL_SPEND_CACHE_TTL_MS = 90000;
|
|
20353
20353
|
var EXTERNAL_SPEND_FETCH_TIMEOUT_MS = 5000;
|
|
20354
|
+
var EXTERNAL_SPEND_PAGE_SIZE = 1000;
|
|
20355
|
+
var EXTERNAL_SPEND_MAX_PAGES = 50;
|
|
20354
20356
|
var LITELLM_MASTER_KEY_STATE_BASENAME = "litellm-master-key";
|
|
20355
20357
|
var BARE_EXTERNAL_NEEDLES = [
|
|
20356
20358
|
"gpt-oss",
|
|
@@ -20447,54 +20449,72 @@ function summarizeExternalSpend(days, now = new Date) {
|
|
|
20447
20449
|
const top = Object.entries(byModel).filter(([, usd]) => usd > 0).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, EXTERNAL_SPEND_TOP_N).map(([name, usd]) => ({ label: shortModelLabel(name), usd }));
|
|
20448
20450
|
return { day24hUsd, day7dUsd, top };
|
|
20449
20451
|
}
|
|
20450
|
-
function
|
|
20451
|
-
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
|
|
20455
|
-
|
|
20456
|
-
|
|
20452
|
+
function normalizeDailyActivityRows(body) {
|
|
20453
|
+
const results = body && typeof body === "object" && Array.isArray(body.results) ? body.results : [];
|
|
20454
|
+
const rows = [];
|
|
20455
|
+
for (const day of results) {
|
|
20456
|
+
if (!day || typeof day !== "object")
|
|
20457
|
+
continue;
|
|
20458
|
+
const models = {};
|
|
20459
|
+
const breakdown = day.breakdown?.models ?? {};
|
|
20460
|
+
for (const [name, entry] of Object.entries(breakdown)) {
|
|
20461
|
+
const raw = entry?.metrics?.spend;
|
|
20462
|
+
const n = typeof raw === "number" ? raw : Number(raw);
|
|
20463
|
+
if (!Number.isFinite(n))
|
|
20464
|
+
continue;
|
|
20465
|
+
models[name] = (models[name] ?? 0) + n;
|
|
20466
|
+
}
|
|
20467
|
+
rows.push({ startTime: day.date, models });
|
|
20457
20468
|
}
|
|
20458
|
-
return
|
|
20469
|
+
return rows;
|
|
20459
20470
|
}
|
|
20460
20471
|
async function fetchAndSummarizeExternalSpend(opts) {
|
|
20461
20472
|
const now = opts.now ?? new Date;
|
|
20462
20473
|
const base = (opts.baseUrl ?? DEFAULT_LITELLM_BASE).replace(/\/+$/, "");
|
|
20463
20474
|
const today = utcDateString(now);
|
|
20464
20475
|
const start = addUtcDays(today, -6);
|
|
20465
|
-
const end = addUtcDays(today, 1);
|
|
20466
|
-
const url = `${base}/spend/logs?start_date=${encodeURIComponent(start)}` + `&end_date=${encodeURIComponent(end)}`;
|
|
20467
20476
|
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
20468
20477
|
const timeoutMs = opts.timeoutMs ?? EXTERNAL_SPEND_FETCH_TIMEOUT_MS;
|
|
20469
|
-
const
|
|
20470
|
-
|
|
20471
|
-
|
|
20472
|
-
const
|
|
20473
|
-
|
|
20474
|
-
headers: {
|
|
20475
|
-
Authorization: `Bearer ${opts.adminKey}`,
|
|
20476
|
-
Accept: "application/json"
|
|
20477
|
-
},
|
|
20478
|
-
signal: ac.signal
|
|
20479
|
-
});
|
|
20480
|
-
if (!res.ok) {
|
|
20481
|
-
console.warn(`external-spend: LiteLLM /spend/logs returned HTTP ${res.status} — ` + `External /usage row will be blank (check master key / proxy)`);
|
|
20482
|
-
return null;
|
|
20483
|
-
}
|
|
20484
|
-
let body;
|
|
20478
|
+
const rows = [];
|
|
20479
|
+
for (let page = 1;page <= EXTERNAL_SPEND_MAX_PAGES; page++) {
|
|
20480
|
+
const url = `${base}/user/daily/activity?start_date=${encodeURIComponent(start)}` + `&end_date=${encodeURIComponent(today)}` + `&page=${page}&page_size=${EXTERNAL_SPEND_PAGE_SIZE}&timezone=0`;
|
|
20481
|
+
const ac = new AbortController;
|
|
20482
|
+
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
20485
20483
|
try {
|
|
20486
|
-
|
|
20484
|
+
const res = await fetchImpl(url, {
|
|
20485
|
+
method: "GET",
|
|
20486
|
+
headers: {
|
|
20487
|
+
Authorization: `Bearer ${opts.adminKey}`,
|
|
20488
|
+
Accept: "application/json"
|
|
20489
|
+
},
|
|
20490
|
+
signal: ac.signal
|
|
20491
|
+
});
|
|
20492
|
+
if (!res.ok) {
|
|
20493
|
+
console.warn(`external-spend: LiteLLM /user/daily/activity returned HTTP ${res.status} — ` + `External /usage row will be blank (check master key / proxy)`);
|
|
20494
|
+
return null;
|
|
20495
|
+
}
|
|
20496
|
+
let body;
|
|
20497
|
+
try {
|
|
20498
|
+
body = await res.json();
|
|
20499
|
+
} catch (err) {
|
|
20500
|
+
console.warn(`external-spend: failed to parse /user/daily/activity JSON: ${err?.message ?? err}`);
|
|
20501
|
+
return null;
|
|
20502
|
+
}
|
|
20503
|
+
rows.push(...normalizeDailyActivityRows(body));
|
|
20504
|
+
const meta = body && typeof body === "object" ? body.metadata : undefined;
|
|
20505
|
+
if (!meta?.has_more)
|
|
20506
|
+
break;
|
|
20507
|
+
if (page === EXTERNAL_SPEND_MAX_PAGES) {
|
|
20508
|
+
console.warn(`external-spend: LiteLLM /user/daily/activity still reports has_more at ` + `page ${EXTERNAL_SPEND_MAX_PAGES} (pagination cap) — External /usage ` + `total is under-reported (collected ${rows.length} day rows)`);
|
|
20509
|
+
}
|
|
20487
20510
|
} catch (err) {
|
|
20488
|
-
console.warn(`external-spend:
|
|
20511
|
+
console.warn(`external-spend: LiteLLM /user/daily/activity fetch failed: ${err?.message ?? err}`);
|
|
20489
20512
|
return null;
|
|
20513
|
+
} finally {
|
|
20514
|
+
clearTimeout(timer);
|
|
20490
20515
|
}
|
|
20491
|
-
return summarizeExternalSpend(normalizeSpendLogRows(body), now);
|
|
20492
|
-
} catch (err) {
|
|
20493
|
-
console.warn(`external-spend: LiteLLM /spend/logs fetch failed: ${err?.message ?? err}`);
|
|
20494
|
-
return null;
|
|
20495
|
-
} finally {
|
|
20496
|
-
clearTimeout(timer);
|
|
20497
20516
|
}
|
|
20517
|
+
return summarizeExternalSpend(rows, now);
|
|
20498
20518
|
}
|
|
20499
20519
|
|
|
20500
20520
|
// src/auth/broker/mirror-write.ts
|