switchroom 0.16.47 → 0.17.0
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/agent-scheduler/index.js +3 -1
- package/dist/auth-broker/index.js +24 -8
- package/dist/cli/drive-write-pretool.mjs +0 -5
- package/dist/cli/notion-write-pretool.mjs +3 -1
- package/dist/cli/switchroom.js +1358 -1030
- package/dist/cli/ui/index.html +84 -12
- package/dist/host-control/main.js +53 -17
- package/dist/vault/approvals/kernel-server.js +4 -1
- package/dist/vault/broker/server.js +201 -56
- package/package.json +3 -3
- package/profiles/_base/cron-session.sh.hbs +1 -1
- package/profiles/_base/start.sh.hbs +54 -3
- package/skills/switchroom-architecture/telegram.md +8 -15
- package/skills/switchroom-cli/SKILL.md +4 -5
- package/skills/telegram-test-harness/SKILL.md +1 -1
- package/telegram-plugin/README.md +18 -29
- package/telegram-plugin/bridge/bridge.ts +1 -41
- package/telegram-plugin/bridge/tool-filter.ts +3 -4
- package/telegram-plugin/dist/bridge/bridge.js +8 -43
- package/telegram-plugin/dist/gateway/gateway.js +682 -773
- package/telegram-plugin/dist/server.js +8 -43
- package/telegram-plugin/gateway/busy-key-reaper.ts +113 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +11 -0
- package/telegram-plugin/gateway/escalation-bridge-gate.ts +46 -0
- package/telegram-plugin/gateway/gate-parity-probe.ts +102 -0
- package/telegram-plugin/gateway/gateway.ts +518 -624
- package/telegram-plugin/gateway/inbound-delivery-confirm.ts +89 -7
- package/telegram-plugin/gateway/inbound-spool.ts +108 -10
- package/telegram-plugin/gateway/model-command.ts +51 -3
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +26 -0
- package/telegram-plugin/gateway/represent-guard.ts +28 -11
- package/telegram-plugin/gateway/status-pin-store.ts +124 -45
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +19 -0
- package/telegram-plugin/history.ts +5 -0
- package/telegram-plugin/hooks/silent-end-interrupt-stop.mjs +1 -2
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +9 -1
- package/telegram-plugin/registry/subagents-schema.ts +126 -1
- package/telegram-plugin/registry/turns-schema.ts +65 -1
- package/telegram-plugin/session-tail.ts +26 -4
- package/telegram-plugin/slot-banner-driver.ts +42 -2
- package/telegram-plugin/status-query-telemetry.ts +100 -0
- package/telegram-plugin/stream-reply-handler.ts +15 -16
- package/telegram-plugin/subagent-watcher.ts +182 -30
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +4 -10
- package/telegram-plugin/tests/busy-key-reaper.test.ts +191 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +11 -17
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +5 -26
- package/telegram-plugin/tests/escalation-bridge-gate.test.ts +38 -0
- package/telegram-plugin/tests/gate-parity-probe.test.ts +171 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +13 -0
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +14 -11
- package/telegram-plugin/tests/inbound-delivery-confirm.test.ts +146 -0
- package/telegram-plugin/tests/inbound-spool.test.ts +143 -0
- package/telegram-plugin/tests/model-command.test.ts +54 -1
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +5 -11
- package/telegram-plugin/tests/nested-worker-visibility-harness.test.ts +329 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +53 -0
- package/telegram-plugin/tests/progress-update-redact.test.ts +99 -0
- package/telegram-plugin/tests/registry-turns.test.ts +67 -0
- package/telegram-plugin/tests/represent-guard.test.ts +42 -6
- package/telegram-plugin/tests/resume-inbound-builder.test.ts +1 -0
- package/telegram-plugin/tests/session-tail.test.ts +10 -1
- package/telegram-plugin/tests/slot-banner-boot-recovery.test.ts +246 -0
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +0 -14
- package/telegram-plugin/tests/status-pin-store.test.ts +220 -5
- package/telegram-plugin/tests/status-query-telemetry.test.ts +115 -0
- package/telegram-plugin/tests/subagent-nested-dispatch.test.ts +209 -0
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +37 -0
- package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +167 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +46 -3
- package/telegram-plugin/tests/subagent-watcher-stall-notification.test.ts +70 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +16 -0
- package/telegram-plugin/tests/tool-filter.test.ts +1 -3
- package/telegram-plugin/tests/tool-label-pretool.test.ts +1 -4
- package/telegram-plugin/tests/turn-flush-safety.test.ts +222 -1
- package/telegram-plugin/tests/worker-activity-feed.test.ts +202 -9
- package/telegram-plugin/tests/worker-feed-dispatch.test.ts +25 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +295 -0
- package/telegram-plugin/tool-activity-summary.ts +19 -0
- package/telegram-plugin/turn-flush-safety.ts +16 -1
- package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +8 -9
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +1 -1
- package/telegram-plugin/uat/scenarios/jtbd-narration-intent-dm.test.ts +1 -1
- package/telegram-plugin/worker-activity-feed.ts +75 -15
- package/vendor/hindsight-memory/CHANGELOG.md +24 -0
- package/vendor/hindsight-memory/README.md +5 -0
- package/vendor/hindsight-memory/scripts/lib/client.py +31 -1
- package/vendor/hindsight-memory/scripts/lib/config.py +41 -2
- package/vendor/hindsight-memory/scripts/lib/content.py +4 -1
- package/vendor/hindsight-memory/scripts/lib/daemon.py +11 -2
- package/vendor/hindsight-memory/scripts/recall.py +74 -1
- package/vendor/hindsight-memory/scripts/retain.py +8 -1
- package/vendor/hindsight-memory/scripts/tests/test_config_client_casts.py +111 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_integration.py +85 -1
- package/vendor/hindsight-memory/scripts/tests/test_recall_tag_filters.py +107 -0
- package/vendor/hindsight-memory/settings.json +4 -0
- package/vendor/hindsight-memory/tests/test_client.py +130 -0
- package/vendor/hindsight-memory/tests/test_config.py +47 -0
- package/vendor/hindsight-memory/tests/test_content.py +18 -0
- package/vendor/hindsight-memory/tests/test_hooks.py +62 -0
- package/telegram-plugin/gateway/error-envelope-card.ts +0 -64
- package/telegram-plugin/gateway/resolve-calling-subagent.ts +0 -78
- package/telegram-plugin/silent-reply.ts +0 -58
- package/telegram-plugin/tests/error-envelope-unlock-card.test.ts +0 -79
- package/telegram-plugin/tests/resolve-calling-subagent.test.ts +0 -269
- package/telegram-plugin/tests/silent-reply-guard.test.ts +0 -122
|
@@ -4151,7 +4151,9 @@ var init_schema = __esm(() => {
|
|
|
4151
4151
|
enabled: exports_external.boolean().optional().describe("Master switch for the session-handoff briefing (default true)."),
|
|
4152
4152
|
max_turns_in_briefing: exports_external.number().int().positive().optional().describe("Cap on recent user/assistant turn pairs fed to the summarizer."),
|
|
4153
4153
|
resume_mode: exports_external.enum(["auto", "continue", "handoff", "none"]).optional().describe("How to resume the next session. 'handoff' (default as of #362) " + "never passes --continue; a fresh Claude starts each restart and " + "reads a briefing assembled from recent Telegram messages, Hindsight " + "recall, and today's daily memory file. 'auto' uses --continue when " + "the latest JSONL is smaller than resume_max_bytes, else falls back " + "to the handoff briefing. 'continue' always passes --continue. " + "'none' starts completely fresh every time."),
|
|
4154
|
-
resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale.")
|
|
4154
|
+
resume_max_bytes: exports_external.number().int().positive().optional().describe("Byte threshold above which 'auto' mode falls back to handoff " + "instead of --continue. Default 2_000_000 (~2MB). Large transcripts " + "can blow out the context window even with prefix caching, and " + "--continue replay is known-fragile at scale."),
|
|
4155
|
+
session_retention_max_count: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): keep at most this many " + "newest session transcripts under .claude/projects; older ones " + "past both this count and the age bound are pruned by the Stop " + "hook. The newest sessions (and the handoff source) are always " + "kept. Default 20; set 0 to disable the count bound."),
|
|
4156
|
+
session_retention_max_age_days: exports_external.number().int().nonnegative().optional().describe("Session-JSONL retention (issue #2792): prune session transcripts " + "older than this many days (a file is deleted only when it is BOTH " + "over the count bound and older than this). Default 30; set 0 to " + "disable the age bound.")
|
|
4155
4157
|
}).optional();
|
|
4156
4158
|
webhookDispatchRule = exports_external.object({
|
|
4157
4159
|
description: exports_external.string().optional(),
|
|
@@ -12411,7 +12413,7 @@ var init_loader = __esm(() => {
|
|
|
12411
12413
|
|
|
12412
12414
|
// src/vault/broker/server.ts
|
|
12413
12415
|
import * as net from "node:net";
|
|
12414
|
-
import { mkdirSync as mkdirSync5, chmodSync as chmodSync4, chownSync, existsSync as
|
|
12416
|
+
import { mkdirSync as mkdirSync5, chmodSync as chmodSync4, chownSync, existsSync as existsSync10, readFileSync as readFileSync8, readdirSync as readdirSync3, statSync as statSync5, unlinkSync as unlinkSync6, writeFileSync, renameSync as renameSync6 } from "node:fs";
|
|
12415
12417
|
|
|
12416
12418
|
// src/agents/compose.ts
|
|
12417
12419
|
import { createHash } from "node:crypto";
|
|
@@ -12482,23 +12484,46 @@ function getPeerCred(fd) {
|
|
|
12482
12484
|
}
|
|
12483
12485
|
}
|
|
12484
12486
|
|
|
12485
|
-
// src/
|
|
12486
|
-
|
|
12487
|
-
var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
|
|
12488
|
-
var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
|
|
12489
|
-
function socketPathToIdentity(socketPath) {
|
|
12487
|
+
// src/broker-common/peercred-path.ts
|
|
12488
|
+
function matchSocketName(socketPath, patterns, maxNameLen) {
|
|
12490
12489
|
if (typeof socketPath !== "string" || socketPath.length === 0)
|
|
12491
12490
|
return null;
|
|
12492
|
-
|
|
12493
|
-
|
|
12491
|
+
let name = null;
|
|
12492
|
+
for (const re of patterns) {
|
|
12493
|
+
const m = socketPath.match(re);
|
|
12494
|
+
if (m && typeof m[1] === "string") {
|
|
12495
|
+
name = m[1];
|
|
12496
|
+
break;
|
|
12497
|
+
}
|
|
12498
|
+
}
|
|
12499
|
+
if (name === null || name.length === 0)
|
|
12500
|
+
return null;
|
|
12501
|
+
if (maxNameLen !== undefined && name.length > maxNameLen)
|
|
12494
12502
|
return null;
|
|
12495
|
-
|
|
12496
|
-
|
|
12503
|
+
return name;
|
|
12504
|
+
}
|
|
12505
|
+
function parseSocketIdentity(socketPath, spec) {
|
|
12506
|
+
const name = matchSocketName(socketPath, spec.patterns, spec.maxNameLen);
|
|
12507
|
+
if (name === null)
|
|
12508
|
+
return null;
|
|
12509
|
+
const operatorName = spec.operatorName ?? "operator";
|
|
12510
|
+
if (name === operatorName)
|
|
12497
12511
|
return { kind: "operator" };
|
|
12498
|
-
if (
|
|
12512
|
+
if (spec.reservedNames.has(name))
|
|
12499
12513
|
return null;
|
|
12500
12514
|
return { kind: "agent", name };
|
|
12501
12515
|
}
|
|
12516
|
+
|
|
12517
|
+
// src/vault/broker/peercred.ts
|
|
12518
|
+
var SOCKET_PATH_AGENT_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\.sock$/;
|
|
12519
|
+
var SOCKET_PATH_AGENT_SUBDIR_RE = /^\/run\/switchroom\/broker\/([a-zA-Z0-9][a-zA-Z0-9_-]*)\/sock$/;
|
|
12520
|
+
var RESERVED_AGENT_NAMES = new Set(["operator", "hostd"]);
|
|
12521
|
+
function socketPathToIdentity(socketPath) {
|
|
12522
|
+
return parseSocketIdentity(socketPath, {
|
|
12523
|
+
patterns: [SOCKET_PATH_AGENT_RE, SOCKET_PATH_AGENT_SUBDIR_RE],
|
|
12524
|
+
reservedNames: RESERVED_AGENT_NAMES
|
|
12525
|
+
});
|
|
12526
|
+
}
|
|
12502
12527
|
function socketPathToAgent(socketPath) {
|
|
12503
12528
|
const identity = socketPathToIdentity(socketPath);
|
|
12504
12529
|
return identity?.kind === "agent" ? identity.name : null;
|
|
@@ -12769,11 +12794,14 @@ import * as path3 from "node:path";
|
|
|
12769
12794
|
import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from "node:crypto";
|
|
12770
12795
|
import {
|
|
12771
12796
|
readFileSync as readFileSync3,
|
|
12772
|
-
|
|
12797
|
+
writeSync as writeSync2,
|
|
12773
12798
|
existsSync as existsSync3,
|
|
12774
12799
|
renameSync,
|
|
12775
12800
|
mkdirSync,
|
|
12776
12801
|
unlinkSync as unlinkSync2,
|
|
12802
|
+
fsyncSync as fsyncSync2,
|
|
12803
|
+
openSync as openSync2,
|
|
12804
|
+
closeSync as closeSync2,
|
|
12777
12805
|
lstatSync,
|
|
12778
12806
|
realpathSync
|
|
12779
12807
|
} from "node:fs";
|
|
@@ -13017,8 +13045,22 @@ function atomicWriteFileSync(path, data, mode) {
|
|
|
13017
13045
|
const dir = dirname(resolve2(effectivePath));
|
|
13018
13046
|
const tmp = resolve2(dir, `.${basename(effectivePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
13019
13047
|
try {
|
|
13020
|
-
|
|
13048
|
+
const fd = openSync2(tmp, "wx", mode);
|
|
13049
|
+
try {
|
|
13050
|
+
writeSync2(fd, data);
|
|
13051
|
+
fsyncSync2(fd);
|
|
13052
|
+
} finally {
|
|
13053
|
+
closeSync2(fd);
|
|
13054
|
+
}
|
|
13021
13055
|
renameSync(tmp, effectivePath);
|
|
13056
|
+
try {
|
|
13057
|
+
const dirFd = openSync2(dir, "r");
|
|
13058
|
+
try {
|
|
13059
|
+
fsyncSync2(dirFd);
|
|
13060
|
+
} finally {
|
|
13061
|
+
closeSync2(dirFd);
|
|
13062
|
+
}
|
|
13063
|
+
} catch {}
|
|
13022
13064
|
} catch (err) {
|
|
13023
13065
|
try {
|
|
13024
13066
|
if (existsSync3(tmp))
|
|
@@ -13289,7 +13331,18 @@ init_loader();
|
|
|
13289
13331
|
|
|
13290
13332
|
// src/vault/auto-unlock.ts
|
|
13291
13333
|
import { createHmac, randomBytes as randomBytes2, createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2 } from "node:crypto";
|
|
13292
|
-
import {
|
|
13334
|
+
import {
|
|
13335
|
+
chmodSync as chmodSync2,
|
|
13336
|
+
closeSync as closeSync4,
|
|
13337
|
+
existsSync as existsSync7,
|
|
13338
|
+
fsyncSync as fsyncSync4,
|
|
13339
|
+
mkdirSync as mkdirSync3,
|
|
13340
|
+
openSync as openSync4,
|
|
13341
|
+
readFileSync as readFileSync7,
|
|
13342
|
+
renameSync as renameSync3,
|
|
13343
|
+
unlinkSync as unlinkSync4,
|
|
13344
|
+
writeSync as writeSync3
|
|
13345
|
+
} from "node:fs";
|
|
13293
13346
|
var FORMAT_VERSION = 1;
|
|
13294
13347
|
var SALT_LEN = 16;
|
|
13295
13348
|
var NONCE_LEN = 12;
|
|
@@ -13959,7 +14012,7 @@ import * as path from "node:path";
|
|
|
13959
14012
|
|
|
13960
14013
|
// src/util/audit-hashchain.ts
|
|
13961
14014
|
import { createHash as createHash4 } from "node:crypto";
|
|
13962
|
-
import { openSync as
|
|
14015
|
+
import { openSync as openSync5, readSync, fstatSync as fstatSync2, closeSync as closeSync5 } from "node:fs";
|
|
13963
14016
|
var CHAIN_GENESIS = "GENESIS";
|
|
13964
14017
|
var SEP = "\x00";
|
|
13965
14018
|
function rowHash(prev, seq, rowJsonNoChain) {
|
|
@@ -13980,7 +14033,7 @@ function chainRow(state, entry) {
|
|
|
13980
14033
|
function readTail(path, tailBytes = 64 * 1024) {
|
|
13981
14034
|
let fd;
|
|
13982
14035
|
try {
|
|
13983
|
-
fd =
|
|
14036
|
+
fd = openSync5(path, "r");
|
|
13984
14037
|
} catch {
|
|
13985
14038
|
return null;
|
|
13986
14039
|
}
|
|
@@ -13996,7 +14049,7 @@ function readTail(path, tailBytes = 64 * 1024) {
|
|
|
13996
14049
|
return null;
|
|
13997
14050
|
} finally {
|
|
13998
14051
|
try {
|
|
13999
|
-
|
|
14052
|
+
closeSync5(fd);
|
|
14000
14053
|
} catch {}
|
|
14001
14054
|
}
|
|
14002
14055
|
}
|
|
@@ -14073,6 +14126,45 @@ function safeAuditLogPath(requestedPath) {
|
|
|
14073
14126
|
}
|
|
14074
14127
|
|
|
14075
14128
|
// src/vault/broker/audit-log.ts
|
|
14129
|
+
var DEFAULT_AUDIT_MAX_BYTES = 32 * 1024 * 1024;
|
|
14130
|
+
var DEFAULT_AUDIT_MAX_FILES = 5;
|
|
14131
|
+
function resolveRotation(opts) {
|
|
14132
|
+
const envBytes = Number(process.env.SWITCHROOM_VAULT_AUDIT_MAX_BYTES);
|
|
14133
|
+
const envFiles = Number(process.env.SWITCHROOM_VAULT_AUDIT_MAX_FILES);
|
|
14134
|
+
const maxBytes = opts.maxBytes !== undefined && opts.maxBytes !== 0 ? opts.maxBytes : Number.isFinite(envBytes) && envBytes !== 0 ? envBytes : DEFAULT_AUDIT_MAX_BYTES;
|
|
14135
|
+
const maxFiles = opts.maxFiles !== undefined && opts.maxFiles > 0 ? opts.maxFiles : Number.isFinite(envFiles) && envFiles > 0 ? envFiles : DEFAULT_AUDIT_MAX_FILES;
|
|
14136
|
+
return { maxBytes, maxFiles };
|
|
14137
|
+
}
|
|
14138
|
+
function rotateAuditLog(logPath, maxFiles) {
|
|
14139
|
+
const oldest = `${logPath}.${maxFiles}`;
|
|
14140
|
+
if (fs.existsSync(oldest)) {
|
|
14141
|
+
try {
|
|
14142
|
+
fs.unlinkSync(oldest);
|
|
14143
|
+
} catch (err) {
|
|
14144
|
+
process.stderr.write(`[vault-audit] ERROR: could not drop oldest rotation ${oldest}: ${err.message}
|
|
14145
|
+
`);
|
|
14146
|
+
}
|
|
14147
|
+
}
|
|
14148
|
+
for (let n = maxFiles - 1;n >= 1; n--) {
|
|
14149
|
+
const from = `${logPath}.${n}`;
|
|
14150
|
+
const to = `${logPath}.${n + 1}`;
|
|
14151
|
+
if (!fs.existsSync(from))
|
|
14152
|
+
continue;
|
|
14153
|
+
try {
|
|
14154
|
+
fs.renameSync(from, to);
|
|
14155
|
+
} catch (err) {
|
|
14156
|
+
process.stderr.write(`[vault-audit] ERROR: could not rotate ${from} → ${to}: ${err.message}
|
|
14157
|
+
`);
|
|
14158
|
+
return;
|
|
14159
|
+
}
|
|
14160
|
+
}
|
|
14161
|
+
try {
|
|
14162
|
+
fs.renameSync(logPath, `${logPath}.1`);
|
|
14163
|
+
} catch (err) {
|
|
14164
|
+
process.stderr.write(`[vault-audit] ERROR: could not rotate active audit log ${logPath}: ${err.message}
|
|
14165
|
+
`);
|
|
14166
|
+
}
|
|
14167
|
+
}
|
|
14076
14168
|
function defaultAuditLogPath() {
|
|
14077
14169
|
return path.join(os.homedir(), ".switchroom", "vault-audit.log");
|
|
14078
14170
|
}
|
|
@@ -14085,8 +14177,17 @@ function callerFromPeer(peer) {
|
|
|
14085
14177
|
function createAuditLogger(opts = {}) {
|
|
14086
14178
|
const logPath = safeAuditLogPath(opts.path ?? defaultAuditLogPath());
|
|
14087
14179
|
let chain = seedChain(logPath);
|
|
14180
|
+
const rotation = resolveRotation(opts);
|
|
14088
14181
|
return {
|
|
14089
14182
|
write(entry) {
|
|
14183
|
+
if (rotation.maxBytes > 0) {
|
|
14184
|
+
try {
|
|
14185
|
+
const size = fs.statSync(logPath).size;
|
|
14186
|
+
if (size >= rotation.maxBytes) {
|
|
14187
|
+
rotateAuditLog(logPath, rotation.maxFiles);
|
|
14188
|
+
}
|
|
14189
|
+
} catch {}
|
|
14190
|
+
}
|
|
14090
14191
|
const { line, next } = chainRow(chain, entry);
|
|
14091
14192
|
let fd;
|
|
14092
14193
|
try {
|
|
@@ -16068,19 +16169,63 @@ var DEFAULT_MAX_TTL_LIFETIME_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
16068
16169
|
|
|
16069
16170
|
// src/vault/grants-db.ts
|
|
16070
16171
|
var DEFAULT_GRANTS_DB_PATH = path2.join(os2.homedir(), ".switchroom", "vault-grants.db");
|
|
16071
|
-
|
|
16072
|
-
|
|
16073
|
-
|
|
16172
|
+
var WAL_SIDECAR_SUFFIXES = ["-wal", "-shm"];
|
|
16173
|
+
function isGrantsDbCorruption(err) {
|
|
16174
|
+
if (!err || typeof err !== "object")
|
|
16175
|
+
return false;
|
|
16176
|
+
const code = String(err.code ?? "").toUpperCase();
|
|
16177
|
+
if (code === "SQLITE_CORRUPT" || code.startsWith("SQLITE_CORRUPT_")) {
|
|
16178
|
+
return true;
|
|
16179
|
+
}
|
|
16180
|
+
const message = String(err.message ?? "").toLowerCase();
|
|
16181
|
+
return message.includes("database disk image is malformed") || message.includes("database is corrupt");
|
|
16182
|
+
}
|
|
16183
|
+
function quarantineGrantsDb(dbPath) {
|
|
16184
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
16185
|
+
const quarantinePath = `${dbPath}.corrupt-${stamp}`;
|
|
16186
|
+
fs2.renameSync(dbPath, quarantinePath);
|
|
16187
|
+
for (const suffix of WAL_SIDECAR_SUFFIXES) {
|
|
16188
|
+
const sidecar = `${dbPath}${suffix}`;
|
|
16189
|
+
if (fs2.existsSync(sidecar)) {
|
|
16190
|
+
try {
|
|
16191
|
+
fs2.renameSync(sidecar, `${quarantinePath}${suffix}`);
|
|
16192
|
+
} catch {}
|
|
16193
|
+
}
|
|
16194
|
+
}
|
|
16195
|
+
return quarantinePath;
|
|
16196
|
+
}
|
|
16197
|
+
function openAndMigrate(dbPath) {
|
|
16074
16198
|
const db = new Database(dbPath, { create: true });
|
|
16075
16199
|
try {
|
|
16076
|
-
|
|
16077
|
-
|
|
16078
|
-
|
|
16079
|
-
|
|
16080
|
-
|
|
16081
|
-
|
|
16200
|
+
try {
|
|
16201
|
+
fs2.chmodSync(dbPath, 384);
|
|
16202
|
+
} catch {}
|
|
16203
|
+
db.run("PRAGMA journal_mode=WAL");
|
|
16204
|
+
db.run("PRAGMA busy_timeout=5000");
|
|
16205
|
+
db.run("PRAGMA foreign_keys=ON");
|
|
16206
|
+
migrateGrantsSchema(db);
|
|
16207
|
+
migrateApprovalSchema(db);
|
|
16208
|
+
} catch (err) {
|
|
16209
|
+
try {
|
|
16210
|
+
db.close();
|
|
16211
|
+
} catch {}
|
|
16212
|
+
throw err;
|
|
16213
|
+
}
|
|
16082
16214
|
return db;
|
|
16083
16215
|
}
|
|
16216
|
+
function openGrantsDb(dbPath = DEFAULT_GRANTS_DB_PATH, opener = openAndMigrate) {
|
|
16217
|
+
const dir = path2.dirname(dbPath);
|
|
16218
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
16219
|
+
try {
|
|
16220
|
+
return opener(dbPath);
|
|
16221
|
+
} catch (err) {
|
|
16222
|
+
if (!isGrantsDbCorruption(err))
|
|
16223
|
+
throw err;
|
|
16224
|
+
const quarantinePath = quarantineGrantsDb(dbPath);
|
|
16225
|
+
console.error(`[vault-broker] grants DB at ${dbPath} is corrupt (${err.message ?? err}); quarantined to ${quarantinePath} and recreating an empty grants DB. ` + "Existing grants must be re-approved via the vault approval flow.");
|
|
16226
|
+
return opener(dbPath);
|
|
16227
|
+
}
|
|
16228
|
+
}
|
|
16084
16229
|
|
|
16085
16230
|
// src/vault/approvals/kernel.ts
|
|
16086
16231
|
import { randomBytes as randomBytes5, randomUUID } from "node:crypto";
|
|
@@ -16431,9 +16576,9 @@ class VaultBroker {
|
|
|
16431
16576
|
chmodSync4(parentDir, 448);
|
|
16432
16577
|
} catch {}
|
|
16433
16578
|
for (const p of [this.socketPath, this.unlockSocketPath]) {
|
|
16434
|
-
if (
|
|
16579
|
+
if (existsSync10(p)) {
|
|
16435
16580
|
try {
|
|
16436
|
-
|
|
16581
|
+
unlinkSync6(p);
|
|
16437
16582
|
} catch {}
|
|
16438
16583
|
}
|
|
16439
16584
|
}
|
|
@@ -16466,9 +16611,9 @@ class VaultBroker {
|
|
|
16466
16611
|
return;
|
|
16467
16612
|
try {
|
|
16468
16613
|
if (ready) {
|
|
16469
|
-
|
|
16470
|
-
} else if (
|
|
16471
|
-
|
|
16614
|
+
writeFileSync(p, "", { mode: 384 });
|
|
16615
|
+
} else if (existsSync10(p)) {
|
|
16616
|
+
unlinkSync6(p);
|
|
16472
16617
|
}
|
|
16473
16618
|
} catch {}
|
|
16474
16619
|
}
|
|
@@ -16500,24 +16645,24 @@ class VaultBroker {
|
|
|
16500
16645
|
try {
|
|
16501
16646
|
entry.server.close();
|
|
16502
16647
|
} catch {}
|
|
16503
|
-
if (
|
|
16648
|
+
if (existsSync10(sockPath)) {
|
|
16504
16649
|
try {
|
|
16505
|
-
|
|
16650
|
+
unlinkSync6(sockPath);
|
|
16506
16651
|
} catch {}
|
|
16507
16652
|
}
|
|
16508
16653
|
}
|
|
16509
16654
|
this.agentServers.clear();
|
|
16510
16655
|
for (const p of [this.socketPath, this.unlockSocketPath]) {
|
|
16511
|
-
if (p &&
|
|
16656
|
+
if (p && existsSync10(p)) {
|
|
16512
16657
|
try {
|
|
16513
|
-
|
|
16658
|
+
unlinkSync6(p);
|
|
16514
16659
|
} catch {}
|
|
16515
16660
|
}
|
|
16516
16661
|
}
|
|
16517
16662
|
try {
|
|
16518
16663
|
const pidPath = resolvePath(PID_FILE_DEFAULT);
|
|
16519
|
-
if (
|
|
16520
|
-
|
|
16664
|
+
if (existsSync10(pidPath))
|
|
16665
|
+
unlinkSync6(pidPath);
|
|
16521
16666
|
} catch {}
|
|
16522
16667
|
}
|
|
16523
16668
|
getStatus() {
|
|
@@ -16542,7 +16687,7 @@ class VaultBroker {
|
|
|
16542
16687
|
return new Promise((resolveP, rejectP) => {
|
|
16543
16688
|
if (abs.endsWith("/sock")) {
|
|
16544
16689
|
const dir = abs.slice(0, -"/sock".length);
|
|
16545
|
-
if (
|
|
16690
|
+
if (existsSync10(dir)) {
|
|
16546
16691
|
try {
|
|
16547
16692
|
chownSync(dir, 0, 0);
|
|
16548
16693
|
} catch {}
|
|
@@ -16551,9 +16696,9 @@ class VaultBroker {
|
|
|
16551
16696
|
} catch {}
|
|
16552
16697
|
}
|
|
16553
16698
|
}
|
|
16554
|
-
if (
|
|
16699
|
+
if (existsSync10(abs)) {
|
|
16555
16700
|
try {
|
|
16556
|
-
|
|
16701
|
+
unlinkSync6(abs);
|
|
16557
16702
|
} catch (err) {
|
|
16558
16703
|
const msg = err instanceof Error ? err.message : String(err);
|
|
16559
16704
|
process.stderr.write(`[vault-broker] could not unlink stale socket agent=${agentName} sock=${abs}: ${msg}
|
|
@@ -16583,9 +16728,9 @@ class VaultBroker {
|
|
|
16583
16728
|
} catch {}
|
|
16584
16729
|
this.agentServers.set(abs, { server, agentName });
|
|
16585
16730
|
const unlockAbs = unlockSocketFor(abs);
|
|
16586
|
-
if (
|
|
16731
|
+
if (existsSync10(unlockAbs)) {
|
|
16587
16732
|
try {
|
|
16588
|
-
|
|
16733
|
+
unlinkSync6(unlockAbs);
|
|
16589
16734
|
} catch {}
|
|
16590
16735
|
}
|
|
16591
16736
|
const unlockServer = net.createServer((sock) => {
|
|
@@ -16638,7 +16783,7 @@ class VaultBroker {
|
|
|
16638
16783
|
if (!Number.isFinite(uid) || uid <= 0)
|
|
16639
16784
|
return;
|
|
16640
16785
|
try {
|
|
16641
|
-
if (
|
|
16786
|
+
if (existsSync10(this.vaultPath))
|
|
16642
16787
|
chownSync(this.vaultPath, uid, uid);
|
|
16643
16788
|
} catch {}
|
|
16644
16789
|
}
|
|
@@ -16651,7 +16796,7 @@ class VaultBroker {
|
|
|
16651
16796
|
const unlockAbs = unlockSocketFor(abs);
|
|
16652
16797
|
if (abs.endsWith("/sock")) {
|
|
16653
16798
|
const dir = abs.slice(0, -"/sock".length);
|
|
16654
|
-
if (
|
|
16799
|
+
if (existsSync10(dir)) {
|
|
16655
16800
|
try {
|
|
16656
16801
|
chownSync(dir, 0, 0);
|
|
16657
16802
|
} catch {}
|
|
@@ -16661,9 +16806,9 @@ class VaultBroker {
|
|
|
16661
16806
|
}
|
|
16662
16807
|
}
|
|
16663
16808
|
for (const p of [abs, unlockAbs]) {
|
|
16664
|
-
if (
|
|
16809
|
+
if (existsSync10(p)) {
|
|
16665
16810
|
try {
|
|
16666
|
-
|
|
16811
|
+
unlinkSync6(p);
|
|
16667
16812
|
} catch {}
|
|
16668
16813
|
}
|
|
16669
16814
|
}
|
|
@@ -17524,8 +17669,8 @@ class VaultBroker {
|
|
|
17524
17669
|
mkdirSync5(tokenDir, { recursive: true });
|
|
17525
17670
|
const tokenPath = path3.join(tokenDir, ".vault-token");
|
|
17526
17671
|
const tmpPath = `${tokenPath}.tmp.${process.pid}`;
|
|
17527
|
-
|
|
17528
|
-
|
|
17672
|
+
writeFileSync(tmpPath, mintResult.token, { mode: 384 });
|
|
17673
|
+
renameSync6(tmpPath, tokenPath);
|
|
17529
17674
|
try {
|
|
17530
17675
|
const uid = allocateAgentUid(agent);
|
|
17531
17676
|
chownSync(tokenPath, uid, uid);
|
|
@@ -17585,9 +17730,9 @@ class VaultBroker {
|
|
|
17585
17730
|
const row = this.grantsDb.query("SELECT agent_slug FROM vault_grants WHERE id = ?").get(id);
|
|
17586
17731
|
if (row && AgentNameSchema.safeParse(row.agent_slug).success) {
|
|
17587
17732
|
const tokenPath = path3.join(os3.homedir(), ".switchroom", "agents", row.agent_slug, ".vault-token");
|
|
17588
|
-
if (
|
|
17733
|
+
if (existsSync10(tokenPath)) {
|
|
17589
17734
|
try {
|
|
17590
|
-
|
|
17735
|
+
unlinkSync6(tokenPath);
|
|
17591
17736
|
} catch {}
|
|
17592
17737
|
}
|
|
17593
17738
|
}
|
|
@@ -17810,7 +17955,7 @@ class VaultBroker {
|
|
|
17810
17955
|
_writePidFile() {
|
|
17811
17956
|
try {
|
|
17812
17957
|
const pidPath = resolvePath(PID_FILE_DEFAULT);
|
|
17813
|
-
|
|
17958
|
+
writeFileSync(pidPath, String(process.pid) + `
|
|
17814
17959
|
`, { mode: 384 });
|
|
17815
17960
|
} catch {}
|
|
17816
17961
|
}
|
|
@@ -17823,10 +17968,10 @@ class VaultBroker {
|
|
|
17823
17968
|
const envPath = process.env.SWITCHROOM_VAULT_BROKER_AUTO_UNLOCK_PATH;
|
|
17824
17969
|
const configuredPath = (envPath && envPath.length > 0 ? envPath : undefined) ?? this.config?.vault?.broker?.autoUnlockCredentialPath ?? DEFAULT_AUTO_UNLOCK_PATH;
|
|
17825
17970
|
const filePath = resolvePath(configuredPath);
|
|
17826
|
-
if (!
|
|
17971
|
+
if (!existsSync10(filePath))
|
|
17827
17972
|
return false;
|
|
17828
17973
|
try {
|
|
17829
|
-
if (
|
|
17974
|
+
if (statSync5(filePath).size === 0)
|
|
17830
17975
|
return false;
|
|
17831
17976
|
} catch {
|
|
17832
17977
|
return false;
|
|
@@ -17938,7 +18083,7 @@ async function main() {
|
|
|
17938
18083
|
const vaultPath = process.env.SWITCHROOM_VAULT_PATH;
|
|
17939
18084
|
let perAgentTargets = [];
|
|
17940
18085
|
try {
|
|
17941
|
-
if (
|
|
18086
|
+
if (existsSync10(perAgentDir)) {
|
|
17942
18087
|
const entries = readdirSync3(perAgentDir, { withFileTypes: true });
|
|
17943
18088
|
const flat = [];
|
|
17944
18089
|
const subdirs = [];
|
|
@@ -17993,7 +18138,7 @@ async function main() {
|
|
|
17993
18138
|
}
|
|
17994
18139
|
const operatorUidStr = process.env.SWITCHROOM_BROKER_OPERATOR_UID;
|
|
17995
18140
|
const operatorDir = "/run/switchroom/broker/operator";
|
|
17996
|
-
if (operatorUidStr !== undefined &&
|
|
18141
|
+
if (operatorUidStr !== undefined && existsSync10(operatorDir)) {
|
|
17997
18142
|
const operatorUid = parseInt(operatorUidStr, 10);
|
|
17998
18143
|
if (!Number.isFinite(operatorUid) || operatorUid <= 0) {
|
|
17999
18144
|
process.stderr.write(`[vault-broker] SWITCHROOM_BROKER_OPERATOR_UID='${operatorUidStr}' is not a positive integer; skipping operator listener
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "switchroom",
|
|
3
3
|
"//version": "NOT the release version — source of truth is the git tag, resolved by scripts/build.mjs:resolveVersion() (see CLAUDE.md > Standard release process). This field is stale by design and only the Layer-4 dev/non-tag fallback for build.mjs + src/cli/resolve-version.ts; do NOT bump it expecting a release to pick it up. npm-pack tarball naming needs a real version — do that as an UNCOMMITTED pack-time bump (see release step 6), never a committed one.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.17.0",
|
|
5
5
|
"description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"build": "node scripts/build.mjs",
|
|
25
25
|
"build:cli": "node scripts/build.mjs && bun build --compile --target=bun-linux-x64 --minify bin/switchroom.ts --outfile switchroom-linux-amd64",
|
|
26
26
|
"pretest": "npm run build",
|
|
27
|
-
"test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/
|
|
27
|
+
"test": "vitest run && bun test telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/registry/api-registry.test.ts telegram-plugin/registry/turns-schema.test.ts telegram-plugin/tests/idle-footer-wiring.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
28
28
|
"test:vitest": "vitest run",
|
|
29
|
-
"test:bun": "bun test src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
29
|
+
"test:bun": "bun test src/vault/grants.test.ts src/vault/grants-db.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/litellm/provision-apply-e2e.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/cross-turn-card-gate.test.ts telegram-plugin/tests/emission-authority-open-gate.test.ts telegram-plugin/tests/emission-authority-ping-gate.test.ts telegram-plugin/tests/emission-authority-card-drain-gate.test.ts telegram-plugin/tests/per-topic-current-turn.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/subagent-nested-dispatch.test.ts telegram-plugin/tests/nested-worker-visibility-harness.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/status-query-telemetry.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/uat/uat-driver.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts telegram-plugin/tests/skill-proposal-card.test.ts",
|
|
30
30
|
"test:watch": "vitest",
|
|
31
31
|
"lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
@@ -95,7 +95,7 @@ cd "{{agentDir}}" || exit 1
|
|
|
95
95
|
# can distinguish cron spend from main-session spend per agent.
|
|
96
96
|
# FAIL-OPEN: missing key OR unreachable proxy → strip routing env, fall back to
|
|
97
97
|
# direct OAuth. An outage must never take the cron session dark.
|
|
98
|
-
if [ -n "$SWITCHROOM_LITELLM" ] && command -v switchroom >/dev/null 2>&1; then
|
|
98
|
+
if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; then
|
|
99
99
|
sr_ll_key="$(switchroom vault get "litellm/{{name}}/api-key" 2>/dev/null || true)"
|
|
100
100
|
sr_ll_ok=""
|
|
101
101
|
if [ -z "$sr_ll_key" ]; then
|
|
@@ -64,7 +64,7 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
64
64
|
#
|
|
65
65
|
# SWITCHROOM_AGENT_NAME is injected by compose env (compose.ts:1816)
|
|
66
66
|
# so it is available here, before the inner-pass export at line ~320.
|
|
67
|
-
if [ -n "$SWITCHROOM_LITELLM" ] && [ -z "$ANTHROPIC_CUSTOM_HEADERS" ] && command -v switchroom >/dev/null 2>&1; then
|
|
67
|
+
if [ -n "${SWITCHROOM_LITELLM:-}" ] && [ -z "$ANTHROPIC_CUSTOM_HEADERS" ] && command -v switchroom >/dev/null 2>&1; then
|
|
68
68
|
sr_ll_key="$(switchroom vault get "litellm/$SWITCHROOM_AGENT_NAME/api-key" 2>/dev/null || true)"
|
|
69
69
|
sr_ll_ok=""
|
|
70
70
|
if [ -z "$sr_ll_key" ]; then
|
|
@@ -657,8 +657,24 @@ mkdir -p "$TELEGRAM_STATE_DIR" 2>/dev/null || true
|
|
|
657
657
|
# .handoff-briefing.md and merged alongside (or instead of) .handoff.md.
|
|
658
658
|
HANDOFF_FILE="{{agentDir}}/.handoff.md"
|
|
659
659
|
HANDOFF_BRIEFING_FILE="{{agentDir}}/.handoff-briefing.md"
|
|
660
|
+
# Decide whether the on-disk briefing is stale and must be rebuilt (#2790).
|
|
661
|
+
# A clean shutdown leaves a non-empty .handoff.md; a *later* hard crash never
|
|
662
|
+
# fires the Stop hook, so that stale briefing would otherwise be re-injected
|
|
663
|
+
# — hiding the crashed session's turns and reorienting the agent from old
|
|
664
|
+
# context. Rebuild when .handoff.md is empty OR when the newest session JSONL
|
|
665
|
+
# is newer than .handoff.md (i.e. a session ran after the briefing was written).
|
|
666
|
+
_LATEST_JSONL=$(_find_latest_jsonl "$CLAUDE_CONFIG_DIR/projects")
|
|
667
|
+
_HANDOFF_STALE=0
|
|
660
668
|
if [ ! -s "$HANDOFF_FILE" ]; then
|
|
661
|
-
|
|
669
|
+
_HANDOFF_STALE=1
|
|
670
|
+
elif [ -n "$_LATEST_JSONL" ]; then
|
|
671
|
+
_JSONL_MOD=$(_stat_mtime "$_LATEST_JSONL")
|
|
672
|
+
_HANDOFF_MOD=$(_stat_mtime "$HANDOFF_FILE")
|
|
673
|
+
if [ "$_JSONL_MOD" -gt "$_HANDOFF_MOD" ]; then
|
|
674
|
+
_HANDOFF_STALE=1
|
|
675
|
+
fi
|
|
676
|
+
fi
|
|
677
|
+
if [ "$_HANDOFF_STALE" = "1" ]; then
|
|
662
678
|
if [ -n "$_LATEST_JSONL" ]; then
|
|
663
679
|
_LAST_MOD=$(_stat_mtime "$_LATEST_JSONL")
|
|
664
680
|
_NOW=$(date +%s)
|
|
@@ -704,6 +720,11 @@ $_HANDOFF_CONTENT"
|
|
|
704
720
|
APPEND_PROMPT="$_HANDOFF_CONTENT"
|
|
705
721
|
fi
|
|
706
722
|
fi
|
|
723
|
+
# Consume the briefing sidecars once injected (#2790). Leaving them on disk
|
|
724
|
+
# lets a later hard crash (which never fires the Stop hook to refresh them)
|
|
725
|
+
# re-serve this now-stale briefing on the next boot. Removing them means the
|
|
726
|
+
# staleness guard above starts from a clean slate every boot.
|
|
727
|
+
rm -f "$HANDOFF_BRIEFING_FILE" "$HANDOFF_FILE" 2>/dev/null || true
|
|
707
728
|
{{else}}
|
|
708
729
|
APPEND_PROMPT={{#if systemPromptAppendShellQuoted}}{{{systemPromptAppendShellQuoted}}}{{else}}""{{/if}}
|
|
709
730
|
{{/if}}
|
|
@@ -807,6 +828,36 @@ if command -v switchroom >/dev/null 2>&1; then
|
|
|
807
828
|
unset sr_wk_pair sr_wk_env sr_wk_key sr_wk_val
|
|
808
829
|
fi
|
|
809
830
|
|
|
831
|
+
# Fleet-wide webkite render config (#2805). Seed the image-baked default
|
|
832
|
+
# (/opt/switchroom/webkite/config.toml) into the XDG location webkite reads
|
|
833
|
+
# ($HOME/.config/webkite/config.toml) so JS-heavy SPA/booking pages render
|
|
834
|
+
# via the baked-in cloakbrowser instead of returning a static shell.
|
|
835
|
+
#
|
|
836
|
+
# REFRESH-from-image, not copy-if-absent. $HOME (/state/agent/home) is a
|
|
837
|
+
# host-PERSISTENT bind mount (see src/agents/compose.ts), so a copy-if-absent
|
|
838
|
+
# seed would write once and then the stale home copy would shadow every future
|
|
839
|
+
# image-shipped config update forever — the config would be un-updatable
|
|
840
|
+
# fleet-wide after first boot. Instead re-sync the target from the baked
|
|
841
|
+
# default whenever they differ, so an image bump always propagates.
|
|
842
|
+
#
|
|
843
|
+
# Operator override still wins: an operator-authored
|
|
844
|
+
# ~/.switchroom/webkite/config.toml is bind-mounted RO onto this exact target
|
|
845
|
+
# (compose.ts). We detect that mount and leave it untouched; even absent the
|
|
846
|
+
# mountpoint check, a cp onto the RO mount fails harmlessly and the override
|
|
847
|
+
# is preserved.
|
|
848
|
+
if [ -f /opt/switchroom/webkite/config.toml ]; then
|
|
849
|
+
sr_wk_target="$HOME/.config/webkite/config.toml"
|
|
850
|
+
if mountpoint -q "$sr_wk_target" 2>/dev/null; then
|
|
851
|
+
: # operator override bind-mounted RO — never clobber it
|
|
852
|
+
elif ! cmp -s /opt/switchroom/webkite/config.toml "$sr_wk_target" 2>/dev/null; then
|
|
853
|
+
mkdir -p "$HOME/.config/webkite" 2>/dev/null || true
|
|
854
|
+
cp /opt/switchroom/webkite/config.toml "$sr_wk_target" 2>/dev/null \
|
|
855
|
+
&& echo "webkite: synced fleet render config to \$HOME/.config/webkite/config.toml" >&2 \
|
|
856
|
+
|| echo "webkite: could not sync render config (non-fatal)" >&2
|
|
857
|
+
fi
|
|
858
|
+
unset sr_wk_target
|
|
859
|
+
fi
|
|
860
|
+
|
|
810
861
|
# LiteLLM routing (opt-in, #litellm). When SWITCHROOM_LITELLM is set (compose
|
|
811
862
|
# env, gated on litellm.enabled && keyConfirmed), route the unmodified `claude`
|
|
812
863
|
# CLI through the operator's LiteLLM proxy at ANTHROPIC_BASE_URL: fetch the
|
|
@@ -833,7 +884,7 @@ fi
|
|
|
833
884
|
# per-session cost/issue tracking is handled out-of-band by correlating
|
|
834
885
|
# LiteLLM's request log against switchroom's turn ledger, never by mutating the
|
|
835
886
|
# claude protocol.
|
|
836
|
-
if [ -n "$SWITCHROOM_LITELLM" ] && command -v switchroom >/dev/null 2>&1; then
|
|
887
|
+
if [ -n "${SWITCHROOM_LITELLM:-}" ] && command -v switchroom >/dev/null 2>&1; then
|
|
837
888
|
sr_ll_key="$(switchroom vault get "litellm/$SWITCHROOM_AGENT_NAME/api-key" 2>/dev/null || true)"
|
|
838
889
|
sr_ll_ok=""
|
|
839
890
|
if [ -z "$sr_ll_key" ]; then
|