switchroom 0.21.7 → 0.21.8
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/tmp-reaper.sh +234 -0
- package/dist/agent-scheduler/index.js +1 -1
- package/dist/auth-broker/index.js +2 -2
- package/dist/cli/notion-write-pretool.mjs +1 -1
- package/dist/cli/switchroom.js +3421 -2744
- package/dist/host-control/main.js +177 -13
- package/dist/vault/approvals/kernel-server.js +2 -2
- package/dist/vault/broker/server.js +2 -2
- package/package.json +5 -4
- package/profiles/_base/start.sh.hbs +115 -0
- package/profiles/_shared/local-time.md.hbs +6 -0
- package/profiles/default/CLAUDE.md.hbs +0 -12
- package/telegram-plugin/dist/gateway/gateway.js +1017 -465
- package/telegram-plugin/gateway/agent-process-liveness.ts +558 -0
- package/telegram-plugin/gateway/approval-hold.ts +32 -1
- package/telegram-plugin/gateway/approval-outcome-sources.ts +274 -0
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +21 -9
- package/telegram-plugin/gateway/callback-query-handlers.ts +87 -15
- package/telegram-plugin/gateway/eval-case-proposal-inbound-builders.ts +197 -0
- package/telegram-plugin/gateway/gateway.ts +12 -10
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +167 -11
- package/telegram-plugin/gateway/self-improve-proposal-wiring.test.ts +333 -0
- package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +152 -3
- package/telegram-plugin/gateway/subagent-handback-marker.ts +19 -0
- package/telegram-plugin/tests/agent-process-liveness.test.ts +406 -0
- package/telegram-plugin/tests/approval-hold-record.test.ts +21 -8
- package/telegram-plugin/tests/boot-resume-gateway-only-respawn.test.ts +752 -0
- package/telegram-plugin/tests/boot-resume-guard-wiring.test.ts +203 -0
- package/telegram-plugin/tests/callback-query-handlers.test.ts +143 -1
- package/telegram-plugin/tests/eval-case-proposal-inbound-builders.test.ts +144 -0
- package/telegram-plugin/tests/hermes-messages-paging.test.ts +149 -0
- package/telegram-plugin/tests/hermes-session-search.test.ts +146 -0
- package/telegram-plugin/tests/pending-inbound-buffer.test.ts +443 -2
- package/telegram-plugin/tests/subagent-handback-marker.test.ts +14 -0
|
@@ -11763,7 +11763,7 @@ var init_schema = __esm(() => {
|
|
|
11763
11763
|
repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
|
|
11764
11764
|
url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
|
|
11765
11765
|
branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
|
|
11766
|
-
})).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated
|
|
11766
|
+
})).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated standing tree for " + "each repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main — an " + "independent clone (own refs/stash, hardlinked objects) seeded from a shared bare " + "clone at ~/.switchroom/repos/<slug>.git. The tree's path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
|
|
11767
11767
|
experimental: exports_external.object({
|
|
11768
11768
|
legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent " + "under the legacy PTY supervisor instead. Default: false."),
|
|
11769
11769
|
legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script " + "behaviour instead of the tmux send-keys path. Default: false.")
|
|
@@ -21565,7 +21565,7 @@ function allocateAgentUid(name) {
|
|
|
21565
21565
|
}
|
|
21566
21566
|
|
|
21567
21567
|
// src/build-info.ts
|
|
21568
|
-
var VERSION = "0.21.
|
|
21568
|
+
var VERSION = "0.21.8";
|
|
21569
21569
|
|
|
21570
21570
|
// src/setup/hindsight-recall-passthrough.ts
|
|
21571
21571
|
var HINDSIGHT_RECALL_TAG_WEIGHT_SEED = Object.freeze({ sidechain: 0.8 });
|
|
@@ -21649,7 +21649,7 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
21649
21649
|
return typeof value === "number" && Number.isFinite(value);
|
|
21650
21650
|
});
|
|
21651
21651
|
var SHARED_FRAGMENTS_DIR = resolve5(PROFILES_ROOT, "_shared");
|
|
21652
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
21652
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol", "local-time"];
|
|
21653
21653
|
for (const name of SHARED_FRAGMENTS) {
|
|
21654
21654
|
const fragPath = join3(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
21655
21655
|
if (existsSync6(fragPath)) {
|
|
@@ -22357,6 +22357,7 @@ import { mkdir as mkdir2, chmod, chown, unlink, appendFile } from "node:fs/promi
|
|
|
22357
22357
|
import {
|
|
22358
22358
|
readdirSync as readdirSync6,
|
|
22359
22359
|
existsSync as existsSync17,
|
|
22360
|
+
statSync as statSync9,
|
|
22360
22361
|
readFileSync as readFileSync13,
|
|
22361
22362
|
writeFileSync as writeFileSync7,
|
|
22362
22363
|
renameSync as renameSync6,
|
|
@@ -22497,13 +22498,14 @@ var AgentSmokeRequestSchema = exports_external.object({
|
|
|
22497
22498
|
deep: exports_external.boolean().optional()
|
|
22498
22499
|
})
|
|
22499
22500
|
});
|
|
22501
|
+
var CANONICAL_CONFIG_PATH = "/state/config/switchroom.yaml";
|
|
22500
22502
|
var ConfigProposeEditRequestSchema = exports_external.object({
|
|
22501
22503
|
...RequestEnvelope,
|
|
22502
22504
|
op: exports_external.literal("config_propose_edit"),
|
|
22503
22505
|
args: exports_external.object({
|
|
22504
22506
|
unified_diff: exports_external.string().min(1).max(MAX_FRAME_BYTES2 - 1024),
|
|
22505
22507
|
reason: exports_external.string().min(1).max(500),
|
|
22506
|
-
target_path: exports_external.literal(
|
|
22508
|
+
target_path: exports_external.literal(CANONICAL_CONFIG_PATH)
|
|
22507
22509
|
})
|
|
22508
22510
|
});
|
|
22509
22511
|
var RequestSchema2 = exports_external.discriminatedUnion("op", [
|
|
@@ -30376,6 +30378,43 @@ function writeFileInPlacePreservingInode(targetPath, content) {
|
|
|
30376
30378
|
throw new Error(`in-place write short: wrote ${buf.length} bytes but read back ${readBack.length}`);
|
|
30377
30379
|
}
|
|
30378
30380
|
}
|
|
30381
|
+
function verifyConfigWriteObserved(configPath, expected, snapshot, proposedChangedPaths) {
|
|
30382
|
+
const expectedBuf = Buffer.from(expected, "utf-8");
|
|
30383
|
+
let observed;
|
|
30384
|
+
let size = null;
|
|
30385
|
+
let mtimeMs = null;
|
|
30386
|
+
try {
|
|
30387
|
+
observed = readFileSync13(configPath);
|
|
30388
|
+
const st = statSync9(configPath);
|
|
30389
|
+
size = st.size;
|
|
30390
|
+
mtimeMs = st.mtimeMs;
|
|
30391
|
+
} catch (e) {
|
|
30392
|
+
return {
|
|
30393
|
+
ok: false,
|
|
30394
|
+
reasons: [`post-write read-back of ${configPath} failed: ${e.message}`],
|
|
30395
|
+
observed: null,
|
|
30396
|
+
size: null,
|
|
30397
|
+
mtimeMs: null
|
|
30398
|
+
};
|
|
30399
|
+
}
|
|
30400
|
+
const reasons = [];
|
|
30401
|
+
if (!observed.equals(expectedBuf)) {
|
|
30402
|
+
let firstDiff = 0;
|
|
30403
|
+
const min = Math.min(observed.length, expectedBuf.length);
|
|
30404
|
+
while (firstDiff < min && observed[firstDiff] === expectedBuf[firstDiff]) {
|
|
30405
|
+
firstDiff += 1;
|
|
30406
|
+
}
|
|
30407
|
+
reasons.push(`written bytes are not observable at ${configPath} ` + `(expected ${expectedBuf.length} bytes, read back ${observed.length}, ` + `first difference at offset ${firstDiff})`);
|
|
30408
|
+
}
|
|
30409
|
+
const observedChangedPaths = classifyBlastRadius(snapshot, observed.toString("utf-8")).changedPaths;
|
|
30410
|
+
const approvedSet = new Set(proposedChangedPaths);
|
|
30411
|
+
const observedSet = new Set(observedChangedPaths);
|
|
30412
|
+
const sameSet = approvedSet.size === observedSet.size && [...approvedSet].every((p) => observedSet.has(p));
|
|
30413
|
+
if (!sameSet) {
|
|
30414
|
+
reasons.push(`on-disk change set diverged from what was approved ` + `(approved: [${[...approvedSet].sort().join(", ")}]; ` + `observed on disk: [${[...observedSet].sort().join(", ")}])`);
|
|
30415
|
+
}
|
|
30416
|
+
return { ok: reasons.length === 0, reasons, observed, size, mtimeMs };
|
|
30417
|
+
}
|
|
30379
30418
|
var STATUS_RETENTION_MS = 10 * 60 * 1000;
|
|
30380
30419
|
var STATUS_MAX_ENTRIES = 256;
|
|
30381
30420
|
var TAIL_BYTES = 4096;
|
|
@@ -30385,7 +30424,71 @@ function isAutoRolloutRequestId(requestId) {
|
|
|
30385
30424
|
return requestId.startsWith(AUTO_ROLLOUT_REQUEST_PREFIX);
|
|
30386
30425
|
}
|
|
30387
30426
|
var AUTO_ROLLOUT_LATCH_FILENAME = "auto-rollout-latch.json";
|
|
30388
|
-
var HOSTD_FALLBACK_CONFIG_PATH =
|
|
30427
|
+
var HOSTD_FALLBACK_CONFIG_PATH = CANONICAL_CONFIG_PATH;
|
|
30428
|
+
function statIdentity(path3) {
|
|
30429
|
+
const st = statSync9(path3);
|
|
30430
|
+
return { dev: st.dev, ino: st.ino };
|
|
30431
|
+
}
|
|
30432
|
+
function checkConfigPathProvenance(writePath, resolveFleetConfig = findConfigFile, identify = statIdentity) {
|
|
30433
|
+
let resolvedPath;
|
|
30434
|
+
try {
|
|
30435
|
+
resolvedPath = resolveFleetConfig();
|
|
30436
|
+
} catch (e) {
|
|
30437
|
+
return {
|
|
30438
|
+
ok: true,
|
|
30439
|
+
writePath,
|
|
30440
|
+
resolvedPath: null,
|
|
30441
|
+
identityChecked: false,
|
|
30442
|
+
detail: `fleet config resolver could not name a config file ` + `(${e.message}) — nothing to compare against ${writePath}`
|
|
30443
|
+
};
|
|
30444
|
+
}
|
|
30445
|
+
if (resolvedPath === writePath) {
|
|
30446
|
+
return {
|
|
30447
|
+
ok: true,
|
|
30448
|
+
writePath,
|
|
30449
|
+
resolvedPath,
|
|
30450
|
+
identityChecked: false,
|
|
30451
|
+
detail: `write target and fleet config path are the same string (${writePath})`
|
|
30452
|
+
};
|
|
30453
|
+
}
|
|
30454
|
+
let write;
|
|
30455
|
+
let fleet;
|
|
30456
|
+
try {
|
|
30457
|
+
write = identify(writePath);
|
|
30458
|
+
fleet = identify(resolvedPath);
|
|
30459
|
+
} catch (e) {
|
|
30460
|
+
return {
|
|
30461
|
+
ok: false,
|
|
30462
|
+
writePath,
|
|
30463
|
+
resolvedPath,
|
|
30464
|
+
identityChecked: false,
|
|
30465
|
+
detail: `write target ${writePath} differs from the fleet config path ` + `${resolvedPath}, and file identity could not be established ` + `(${e.message}) — compared as strings, not inodes`
|
|
30466
|
+
};
|
|
30467
|
+
}
|
|
30468
|
+
if (write.dev === fleet.dev && write.ino === fleet.ino) {
|
|
30469
|
+
return {
|
|
30470
|
+
ok: true,
|
|
30471
|
+
writePath,
|
|
30472
|
+
resolvedPath,
|
|
30473
|
+
identityChecked: true,
|
|
30474
|
+
detail: `write target ${writePath} and fleet config path ${resolvedPath} are ` + `two names for the SAME file (dev=${write.dev} ino=${write.ino})`
|
|
30475
|
+
};
|
|
30476
|
+
}
|
|
30477
|
+
return {
|
|
30478
|
+
ok: false,
|
|
30479
|
+
writePath,
|
|
30480
|
+
resolvedPath,
|
|
30481
|
+
identityChecked: true,
|
|
30482
|
+
detail: `write target ${writePath} (dev=${write.dev} ino=${write.ino}) is a ` + `DIFFERENT file from the config the fleet reads, ${resolvedPath} ` + `(dev=${fleet.dev} ino=${fleet.ino}) — a write here would be invisible ` + `to every other reader`
|
|
30483
|
+
};
|
|
30484
|
+
}
|
|
30485
|
+
var CONFIG_PATH_PROVENANCE_TAG = "hostd-config-path-provenance";
|
|
30486
|
+
function configPathProvenanceWarning(resolveFleetConfig = findConfigFile, identify = statIdentity) {
|
|
30487
|
+
const prov = checkConfigPathProvenance(CANONICAL_CONFIG_PATH, resolveFleetConfig, identify);
|
|
30488
|
+
if (prov.ok)
|
|
30489
|
+
return null;
|
|
30490
|
+
return `${CONFIG_PATH_PROVENANCE_TAG}: ${prov.detail}. ` + `config_propose_edit will REFUSE with E_CONFIG_PATH_MISMATCH until this ` + `agrees — check the hostd bind mount over ${CANONICAL_CONFIG_PATH} and ` + `the SWITCHROOM_CONFIG it exports alongside it.`;
|
|
30491
|
+
}
|
|
30389
30492
|
function resolveHostdConfigPath(explicit) {
|
|
30390
30493
|
if (explicit)
|
|
30391
30494
|
return explicit;
|
|
@@ -31572,6 +31675,12 @@ class HostdServer {
|
|
|
31572
31675
|
return err("E_CONFIG_EDIT_DISABLED", "config_propose_edit is disabled").why("operator opt-in per RFC §3.3").fixFlipFlag("hostd.config_edit_enabled", true).docs("https://switchroom.dev/docs/config-edit#opt-in").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
|
|
31573
31676
|
}
|
|
31574
31677
|
const configPath = this.opts.configPath ?? req.args.target_path;
|
|
31678
|
+
if (this.opts.configPath === undefined) {
|
|
31679
|
+
const provenance = checkConfigPathProvenance(configPath, this.opts.resolveFleetConfigPath ?? findConfigFile, this.opts.identifyForProvenance ?? statIdentity);
|
|
31680
|
+
if (!provenance.ok) {
|
|
31681
|
+
return this.configPathMismatch(provenance.detail, req, caller, started);
|
|
31682
|
+
}
|
|
31683
|
+
}
|
|
31575
31684
|
const verdict = validateConfigEdit({
|
|
31576
31685
|
configPath,
|
|
31577
31686
|
targetPath: req.args.target_path,
|
|
@@ -31746,10 +31855,10 @@ class HostdServer {
|
|
|
31746
31855
|
}
|
|
31747
31856
|
}
|
|
31748
31857
|
try {
|
|
31749
|
-
|
|
31858
|
+
this.writeLiveConfig(configPath, postApplyFresh);
|
|
31750
31859
|
} catch (e) {
|
|
31751
31860
|
try {
|
|
31752
|
-
|
|
31861
|
+
this.writeLiveConfig(configPath, snapshot);
|
|
31753
31862
|
} catch {}
|
|
31754
31863
|
await approval.finalize({
|
|
31755
31864
|
outcome: "reconcile_failed_rolled_back",
|
|
@@ -31757,8 +31866,31 @@ class HostdServer {
|
|
|
31757
31866
|
});
|
|
31758
31867
|
return this.reconcileFailedRolledBack(`write failed: ${e.message}`, req, caller, started);
|
|
31759
31868
|
}
|
|
31760
|
-
const
|
|
31761
|
-
|
|
31869
|
+
const observation = verifyConfigWriteObserved(configPath, postApplyFresh, snapshot, proposedChangedPaths);
|
|
31870
|
+
if (!observation.ok) {
|
|
31871
|
+
const why = `post-write verification failed: ${observation.reasons.join("; ")}`;
|
|
31872
|
+
const snapshotBuf = Buffer.from(snapshot, "utf-8");
|
|
31873
|
+
const alreadySnapshot = observation.observed !== null && observation.observed.equals(snapshotBuf);
|
|
31874
|
+
let restoreDetail;
|
|
31875
|
+
if (alreadySnapshot) {
|
|
31876
|
+
restoreDetail = "live config already byte-identical to the pre-write snapshot; no restore needed";
|
|
31877
|
+
} else {
|
|
31878
|
+
try {
|
|
31879
|
+
this.writeLiveConfig(configPath, snapshot);
|
|
31880
|
+
restoreDetail = "rolled back to the pre-write snapshot";
|
|
31881
|
+
} catch (e) {
|
|
31882
|
+
restoreDetail = `SNAPSHOT RESTORE ALSO FAILED: ${e.message} — ` + `live config at ${configPath} is in an UNKNOWN state, inspect it by hand`;
|
|
31883
|
+
}
|
|
31884
|
+
}
|
|
31885
|
+
await approval.finalize({
|
|
31886
|
+
outcome: "reconcile_failed_rolled_back",
|
|
31887
|
+
detail: `${why}; ${restoreDetail}`
|
|
31888
|
+
});
|
|
31889
|
+
return this.writeNotObserved(`${why}; ${restoreDetail}`, req, caller, started);
|
|
31890
|
+
}
|
|
31891
|
+
const reconcileEnv = { SWITCHROOM_CONFIG: configPath };
|
|
31892
|
+
const runner = this.opts.runReconcile ?? (async () => this.runSwitchroom(caller.kind === "agent" ? ["apply", "--only", callerName, "--non-interactive"] : ["apply", "--non-interactive"], reconcileEnv));
|
|
31893
|
+
const recRes = await runner({ requestId: approvalId, env: reconcileEnv });
|
|
31762
31894
|
if (recRes.exit_code === 0) {
|
|
31763
31895
|
const blast = classifyBlastRadius(snapshot, postApplyFresh);
|
|
31764
31896
|
await approval.finalize({
|
|
@@ -31766,19 +31898,21 @@ class HostdServer {
|
|
|
31766
31898
|
affectedAgents: blast.agents,
|
|
31767
31899
|
fleetWide: blast.fleetWide
|
|
31768
31900
|
});
|
|
31901
|
+
const writeEvidence = `config write observed: path=${configPath} ` + `size=${observation.size} mtime_ms=${observation.mtimeMs}`;
|
|
31769
31902
|
return {
|
|
31770
31903
|
v: 1,
|
|
31771
31904
|
request_id: req.request_id,
|
|
31772
31905
|
result: "completed",
|
|
31773
31906
|
exit_code: 0,
|
|
31774
31907
|
duration_ms: Date.now() - started,
|
|
31775
|
-
stdout_tail: tail(recRes.stdout
|
|
31908
|
+
stdout_tail: tail(`${recRes.stdout}
|
|
31909
|
+
--- ${writeEvidence} ---`),
|
|
31776
31910
|
stderr_tail: tail(recRes.stderr)
|
|
31777
31911
|
};
|
|
31778
31912
|
}
|
|
31779
31913
|
let rollbackDetail = "";
|
|
31780
31914
|
try {
|
|
31781
|
-
|
|
31915
|
+
this.writeLiveConfig(configPath, snapshot);
|
|
31782
31916
|
} catch (e) {
|
|
31783
31917
|
rollbackDetail = `snapshot restore failed: ${e.message}`;
|
|
31784
31918
|
await approval.finalize({
|
|
@@ -31787,7 +31921,7 @@ class HostdServer {
|
|
|
31787
31921
|
});
|
|
31788
31922
|
return this.reconcileFailedRolledBack(rollbackDetail, req, caller, started);
|
|
31789
31923
|
}
|
|
31790
|
-
const recRes2 = await runner({ requestId: approvalId });
|
|
31924
|
+
const recRes2 = await runner({ requestId: approvalId, env: reconcileEnv });
|
|
31791
31925
|
const recoveryNote = recRes2.exit_code === 0 ? "rolled back successfully" : `rolled back but recovery reconcile also failed (exit ${recRes2.exit_code})`;
|
|
31792
31926
|
await approval.finalize({
|
|
31793
31927
|
outcome: "reconcile_failed_rolled_back",
|
|
@@ -31803,6 +31937,27 @@ class HostdServer {
|
|
|
31803
31937
|
const built = err("E_CONFIG_CHANGED", "config changed since the proposal was validated; re-propose against the current config").why(why).fixBadInput("unified_diff").op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).asDenied().build(req.request_id, Date.now() - started);
|
|
31804
31938
|
return { ...built, error: legacy };
|
|
31805
31939
|
}
|
|
31940
|
+
writeLiveConfig(path3, content) {
|
|
31941
|
+
(this.opts.writeConfigFile ?? writeFileInPlacePreservingInode)(path3, content);
|
|
31942
|
+
}
|
|
31943
|
+
writeNotObserved(detail, req, caller, started) {
|
|
31944
|
+
const legacy = `E_WRITE_NOT_OBSERVED: ${detail}`;
|
|
31945
|
+
const built = err("E_WRITE_NOT_OBSERVED", "config write could not be observed on the target file; apply aborted before reconcile").why(detail).fixOperatorAction("infra", [
|
|
31946
|
+
"confirm hostd reads and writes the SAME switchroom.yaml the fleet reads (check the hostd bind mount over /state/config/switchroom.yaml)",
|
|
31947
|
+
"check for a competing writer to the config (operator hand-edit or editor daemon) during the apply window",
|
|
31948
|
+
"inspect the live config by hand before re-proposing — the diff was valid, the write path was not"
|
|
31949
|
+
]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
31950
|
+
return { ...built, error: legacy };
|
|
31951
|
+
}
|
|
31952
|
+
configPathMismatch(detail, req, caller, started) {
|
|
31953
|
+
const legacy = `E_CONFIG_PATH_MISMATCH: ${detail}`;
|
|
31954
|
+
const built = err("E_CONFIG_PATH_MISMATCH", "hostd would write a different switchroom.yaml than the fleet reads; apply refused before any write").why(detail).fixOperatorAction("infra", [
|
|
31955
|
+
`confirm the hostd container bind-mounts the live switchroom.yaml onto ${CANONICAL_CONFIG_PATH}`,
|
|
31956
|
+
"confirm SWITCHROOM_CONFIG inside the hostd container points at that same path (hostd install exports it alongside the mount)",
|
|
31957
|
+
"re-propose once the two agree — the diff was never applied, nothing was written"
|
|
31958
|
+
]).op("config_propose_edit").caller(caller.kind === "agent" ? "agent" : "operator").agentName(caller.kind === "agent" ? caller.name : undefined).build(req.request_id, Date.now() - started);
|
|
31959
|
+
return { ...built, error: legacy };
|
|
31960
|
+
}
|
|
31806
31961
|
reconcileFailedRolledBack(detail, req, caller, started, output) {
|
|
31807
31962
|
const legacy = `E_RECONCILE_FAILED_ROLLED_BACK: ${detail}`;
|
|
31808
31963
|
const built = err("E_RECONCILE_FAILED_ROLLED_BACK", "config write or reconcile failed; live file rolled back to snapshot").why(detail).fixOperatorAction("infra", [
|
|
@@ -31878,7 +32033,11 @@ ${output.recovery.stderr}` : "";
|
|
|
31878
32033
|
name: "bot_token",
|
|
31879
32034
|
cmd: "test -f /state/agent/telegram/.env && grep -qE '^TELEGRAM_BOT_TOKEN=[0-9]+:' /state/agent/telegram/.env"
|
|
31880
32035
|
},
|
|
31881
|
-
{ name: "state", cmd: "test -w /state/agent" }
|
|
32036
|
+
{ name: "state", cmd: "test -w /state/agent" },
|
|
32037
|
+
{
|
|
32038
|
+
name: "tzdata",
|
|
32039
|
+
cmd: "test ! -L /etc/localtime && python3 -c 'import sys;" + "from datetime import datetime;" + "from zoneinfo import ZoneInfo;" + 'sys.exit(0 if datetime.now(ZoneInfo("Etc/UTC")).utcoffset()' + ".total_seconds()==0 else 1)'"
|
|
32040
|
+
}
|
|
31882
32041
|
];
|
|
31883
32042
|
if (req.args.deep) {
|
|
31884
32043
|
PROBES.push({
|
|
@@ -34146,6 +34305,11 @@ async function main() {
|
|
|
34146
34305
|
`);
|
|
34147
34306
|
process.exit(2);
|
|
34148
34307
|
}
|
|
34308
|
+
const provenanceWarning = configPathProvenanceWarning();
|
|
34309
|
+
if (provenanceWarning !== null) {
|
|
34310
|
+
process.stderr.write(`hostd: ${provenanceWarning}
|
|
34311
|
+
`);
|
|
34312
|
+
}
|
|
34149
34313
|
const agentUids = {};
|
|
34150
34314
|
for (const name of Object.keys(config.agents)) {
|
|
34151
34315
|
agentUids[name] = allocateAgentUid(name);
|
|
@@ -5203,7 +5203,7 @@ var init_schema = __esm(() => {
|
|
|
5203
5203
|
repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
|
|
5204
5204
|
url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
|
|
5205
5205
|
branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
|
|
5206
|
-
})).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated
|
|
5206
|
+
})).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated standing tree for " + "each repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main — an " + "independent clone (own refs/stash, hardlinked objects) seeded from a shared bare " + "clone at ~/.switchroom/repos/<slug>.git. The tree's path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
|
|
5207
5207
|
experimental: exports_external.object({
|
|
5208
5208
|
legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent " + "under the legacy PTY supervisor instead. Default: false."),
|
|
5209
5209
|
legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script " + "behaviour instead of the tmux send-keys path. Default: false.")
|
|
@@ -19544,7 +19544,7 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
19544
19544
|
return typeof value === "number" && Number.isFinite(value);
|
|
19545
19545
|
});
|
|
19546
19546
|
var SHARED_FRAGMENTS_DIR = resolve5(PROFILES_ROOT, "_shared");
|
|
19547
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
19547
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol", "local-time"];
|
|
19548
19548
|
for (const name of SHARED_FRAGMENTS) {
|
|
19549
19549
|
const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
19550
19550
|
if (existsSync5(fragPath)) {
|
|
@@ -4799,7 +4799,7 @@ var init_schema = __esm(() => {
|
|
|
4799
4799
|
repos: exports_external.record(exports_external.string().regex(/^[a-z0-9][a-z0-9-]*$/, "Repo slug must be kebab-case ASCII: start with a lowercase letter or digit, contain only lowercase letters, digits, and hyphens"), exports_external.object({
|
|
4800
4800
|
url: exports_external.string().min(1).describe("Git remote URL for the repo (e.g. 'git@github.com:org/repo.git' or " + "'https://github.com/org/repo.git'). Used verbatim for git clone."),
|
|
4801
4801
|
branch_default: exports_external.string().optional().describe("Default branch to track (defaults to the remote's HEAD, typically 'main'). " + "The per-agent branch 'agent/<agentName>/main' fast-forwards to this branch " + "when the worktree is clean on session start.")
|
|
4802
|
-
})).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated
|
|
4802
|
+
})).optional().describe("Repos this agent operates on. Switchroom provisions a dedicated standing tree for " + "each repo at <agentDir>/work/<slug>/ on branch agent/<agentName>/main — an " + "independent clone (own refs/stash, hardlinked objects) seeded from a shared bare " + "clone at ~/.switchroom/repos/<slug>.git. The tree's path is injected " + "into the agent's environment as SWITCHROOM_REPO_<SLUG_UPPER>. " + "Agents without this field continue to work unchanged."),
|
|
4803
4803
|
experimental: exports_external.object({
|
|
4804
4804
|
legacy_pty: exports_external.boolean().optional().describe("Opt out of the default tmux supervisor (#725) and run the agent " + "under the legacy PTY supervisor instead. Default: false."),
|
|
4805
4805
|
legacy_autoaccept_expect: exports_external.boolean().optional().describe("Opt the autoaccept gateway back into the legacy expect-script " + "behaviour instead of the tmux send-keys path. Default: false.")
|
|
@@ -19957,7 +19957,7 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
19957
19957
|
return typeof value === "number" && Number.isFinite(value);
|
|
19958
19958
|
});
|
|
19959
19959
|
var SHARED_FRAGMENTS_DIR = resolve5(PROFILES_ROOT, "_shared");
|
|
19960
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
19960
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol", "local-time"];
|
|
19961
19961
|
for (const name of SHARED_FRAGMENTS) {
|
|
19962
19962
|
const fragPath = join2(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
19963
19963
|
if (existsSync5(fragPath)) {
|
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.21.
|
|
4
|
+
"version": "0.21.8",
|
|
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,11 +24,11 @@
|
|
|
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/agent-state-dir-preload.test.ts telegram-plugin/tests/hindsight-bank-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/boot-briefing-builder.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/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.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/progress-fallback-cap.test.ts telegram-plugin/tests/progress-cap.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/worker-origin-gap-dispatch.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",
|
|
27
|
+
"test": "vitest run && bun test telegram-plugin/tests/hermes-messages-paging.test.ts telegram-plugin/tests/hermes-session-search.test.ts telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/hindsight-bank-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/boot-briefing-builder.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/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.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/progress-fallback-cap.test.ts telegram-plugin/tests/progress-cap.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/worker-origin-gap-dispatch.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 telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/hindsight-bank-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts 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-scope-persist.test.ts src/vault/broker/server-tokenless-scope.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/self-approval-bypass.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/boot-briefing-builder.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/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.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/progress-fallback-cap.test.ts telegram-plugin/tests/progress-cap.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/worker-origin-gap-dispatch.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",
|
|
29
|
+
"test:bun": "bun test telegram-plugin/tests/hermes-messages-paging.test.ts telegram-plugin/tests/hermes-session-search.test.ts telegram-plugin/tests/agent-state-dir-preload.test.ts telegram-plugin/tests/hindsight-bank-preload.test.ts telegram-plugin/tests/catch-all-forwarded-history.test.ts 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-scope-persist.test.ts src/vault/broker/server-tokenless-scope.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/self-approval-bypass.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/boot-briefing-builder.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/ipc-server-buzz-dedup.test.ts telegram-plugin/tests/ipc-server-query-pending-permission.test.ts telegram-plugin/tests/ipc-server-check-pre-approved.test.ts telegram-plugin/tests/rollout-narration-edit-socket.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/progress-fallback-cap.test.ts telegram-plugin/tests/progress-cap.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/worker-origin-gap-dispatch.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
|
-
"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-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-bench-baseline-anonymised.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-hindsight-bank-hermeticity.mjs && node scripts/check-parked-turn-start-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-ctx-send-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-foreign-db-readonly.mjs && node scripts/check-changelog-entry.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts && bun scripts/check-hostd-template-guard.ts",
|
|
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-test-runner-coverage.mjs && node scripts/check-bun-module-mock-scope.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-bench-baseline-anonymised.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-auth-test-hermeticity.mjs && node scripts/check-agent-state-dir-hermeticity.mjs && node scripts/check-hindsight-bank-hermeticity.mjs && node scripts/check-parked-turn-start-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-mcp-instructions-budget.mjs && node scripts/check-web-subscription-honest.mjs && node scripts/check-no-unpinned-npx-playwright.mjs && node scripts/check-gateway-line-ratchet.mjs && node scripts/check-retry-flood-hooks.mjs && node scripts/check-callback-ctx-wrapping.mjs && node scripts/check-ctx-send-wrapping.mjs && node scripts/check-status-pin-single-path.mjs && node scripts/check-litellm-config-guard.mjs && node scripts/check-release-asset-names.mjs && node scripts/check-foreign-db-readonly.mjs && node scripts/check-claude-cli-lockstep.mjs && node scripts/check-changelog-entry.mjs && node scripts/check-agent-attribution-trailers.mjs && node scripts/check-hindsight-write-redaction.mjs && bun scripts/check-secret-pattern-parity.ts && bun scripts/check-hostd-template-guard.ts",
|
|
32
32
|
"lint:tsc": "tsc --noEmit",
|
|
33
33
|
"lint:hindsight-write-redaction": "node scripts/check-hindsight-write-redaction.mjs",
|
|
34
34
|
"lint:secret-pattern-parity": "bun scripts/check-secret-pattern-parity.ts",
|
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
"lint:litellm-config-guard": "node scripts/check-litellm-config-guard.mjs",
|
|
54
54
|
"lint:release-asset-contract": "node scripts/check-release-asset-names.mjs",
|
|
55
55
|
"lint:foreign-db-readonly": "node scripts/check-foreign-db-readonly.mjs",
|
|
56
|
+
"lint:claude-cli-lockstep": "node scripts/check-claude-cli-lockstep.mjs",
|
|
56
57
|
"lint:changelog-entry": "node scripts/check-changelog-entry.mjs",
|
|
57
58
|
"changelog:generate": "node scripts/gen-changelog-entry.mjs",
|
|
58
59
|
"lint:agent-attribution-trailers": "node scripts/check-agent-attribution-trailers.mjs",
|
|
@@ -33,6 +33,31 @@ if [ "$SWITCHROOM_RUNTIME" = "docker" ] && [ -z "$SWITCHROOM_DOCKER_TMUX_INNER"
|
|
|
33
33
|
# same path the rest of start.sh + the MCP sidecar expects.
|
|
34
34
|
export TELEGRAM_STATE_DIR="{{agentDir}}/telegram"
|
|
35
35
|
|
|
36
|
+
# --- Boot-resume generation token: OPEN it (switchroom#4641) -------------
|
|
37
|
+
#
|
|
38
|
+
# The gateway's boot-resume block (orphan-turn reaper, `resume_interrupted`
|
|
39
|
+
# synthetic, bridge-dead marker, `.pending-turn.env`) is a ONCE-PER-CONTAINER-
|
|
40
|
+
# BOOT action. But the gateway is a SUPERVISED SIDECAR: when it crashes the
|
|
41
|
+
# supervisor respawns it and its boot block runs again — against a claude
|
|
42
|
+
# session that never stopped. That is #4641: it stamped the still-executing
|
|
43
|
+
# turn `ended_via='restart'` and told the live session it had been killed.
|
|
44
|
+
#
|
|
45
|
+
# THIS line is the whole generation mechanism. Deleting the token here — in
|
|
46
|
+
# the outer pass, exactly once per container boot, BEFORE the gateway is
|
|
47
|
+
# forked below and where the supervisor can never re-run it — is what makes
|
|
48
|
+
# "the token exists" mean "a gateway in this same container generation
|
|
49
|
+
# already completed its boot resume". The gateway writes the token at the
|
|
50
|
+
# END of that block (agent-process-liveness.ts `markBootResumeComplete`) and
|
|
51
|
+
# skips the block entirely when it finds one. No timing, no clock
|
|
52
|
+
# comparison: a gateway that dies mid-boot leaves no token, so its
|
|
53
|
+
# replacement redoes the work and a genuinely interrupted turn is never lost.
|
|
54
|
+
#
|
|
55
|
+
# `agent-process.json` (written by the inner pass just before it `exec`s the
|
|
56
|
+
# agent) goes too: it names a pid from the PREVIOUS generation, and the
|
|
57
|
+
# pid namespace is reset by a container restart, so leaving it risks a
|
|
58
|
+
# (pid, starttime) collision against an unrelated live process.
|
|
59
|
+
rm -f "$TELEGRAM_STATE_DIR/.boot-resume-done" "$TELEGRAM_STATE_DIR/agent-process.json" 2>/dev/null || true
|
|
60
|
+
|
|
36
61
|
# SWITCHROOM_AGENT_NAME is the canonical "which agent am I" identity. It is
|
|
37
62
|
# normally supplied by the container env (compose.ts) so it is already
|
|
38
63
|
# present here, but the authoritative inner-pass export lives at line ~387,
|
|
@@ -538,6 +563,32 @@ x-litellm-tags: agent:$SWITCHROOM_AGENT_NAME,profile:${SWITCHROOM_AGENT_PROFILE:
|
|
|
538
563
|
bun /opt/switchroom/agent-scheduler/index.js &
|
|
539
564
|
fi
|
|
540
565
|
|
|
566
|
+
# 3a) switchroom-tmp-reaper — bounded janitor for the /tmp tmpfs.
|
|
567
|
+
#
|
|
568
|
+
# /tmp is a RAM-backed tmpfs sized by `resources.tmp_size`
|
|
569
|
+
# (DEFAULT_TMP_SIZE = 2g) and mounted noexec by Docker's tmpfs
|
|
570
|
+
# defaults. Nothing ages out of a tmpfs, so a long-running container
|
|
571
|
+
# accumulates every scratch dir any tool forgot to remove until writes
|
|
572
|
+
# start failing ENOSPC — and the failure surfaces in the victim (npm
|
|
573
|
+
# ci, git, the CLI's own staging), never at the cause. Observed live:
|
|
574
|
+
# 4,462 top-level entries and 490 MiB used, 204 MiB of it two orphaned
|
|
575
|
+
# compile artifacts from one test run.
|
|
576
|
+
#
|
|
577
|
+
# Deliberately NOT "raise tmp_size": a bigger tmpfs converts a fast,
|
|
578
|
+
# loud failure into a slow leak against host RAM. The safety contract
|
|
579
|
+
# (age floor, open-file check, /tmp-only scope) lives in the script's
|
|
580
|
+
# header and is pinned by tests/tmp-reaper.test.ts.
|
|
581
|
+
#
|
|
582
|
+
# Kill switch: SWITCHROOM_TMP_REAPER=0 in the container env. Supervised
|
|
583
|
+
# like the other sidecars so a crash self-heals and its log rotates;
|
|
584
|
+
# NOT --oneshot-ok, because a clean exit from an endless loop is
|
|
585
|
+
# abnormal and must restart.
|
|
586
|
+
if [ "$SWITCHROOM_TMP_REAPER" != "0" ] \
|
|
587
|
+
&& [ -x /opt/switchroom/bin/tmp-reaper.sh ]; then
|
|
588
|
+
_switchroom_supervise tmp-reaper /var/log/switchroom/tmp-reaper.log \
|
|
589
|
+
bash /opt/switchroom/bin/tmp-reaper.sh &
|
|
590
|
+
fi
|
|
591
|
+
|
|
541
592
|
# 3b) Buzz co-channel inbound sidecar (Phase 1, channels.buzz).
|
|
542
593
|
# A supervised sibling that opens a WebSocket Nostr subscription to a
|
|
543
594
|
# closed Buzz relay, NIP-42-authenticates, and injects allowlisted
|
|
@@ -2475,6 +2526,70 @@ fi
|
|
|
2475
2526
|
rm -f "$_session_model_resolved_tmp"
|
|
2476
2527
|
unset _session_model_resolved_tmp
|
|
2477
2528
|
|
|
2529
|
+
# --- Agent-process identity record (switchroom#4641) ---
|
|
2530
|
+
#
|
|
2531
|
+
# The gateway and the agent are SEPARATE processes: the gateway runs as a
|
|
2532
|
+
# supervised sidecar (see `_switchroom_supervise gateway` above) while THIS
|
|
2533
|
+
# shell becomes the agent via `exec claude` below. A gateway crash respawns
|
|
2534
|
+
# only the gateway — this process, its conversation, its in-flight turn and
|
|
2535
|
+
# its sub-agents all keep running. The gateway used to read its own boot as
|
|
2536
|
+
# "the agent restarted" and fire a false `resume_interrupted` into the live
|
|
2537
|
+
# session ("your previous turn was interrupted", "your sub-agents were killed")
|
|
2538
|
+
# while everything it named was demonstrably still alive.
|
|
2539
|
+
#
|
|
2540
|
+
# What DECIDES that is the per-container-boot generation token cleared in the
|
|
2541
|
+
# outer pass above (`.boot-resume-done`). This record is the SAFETY VETO on top
|
|
2542
|
+
# of it: if the token says "already done" but the recorded agent process is
|
|
2543
|
+
# provably gone, the gateway runs the boot resume anyway. It can only ever
|
|
2544
|
+
# re-enable a resume, never suppress a legitimate one — which is why nothing
|
|
2545
|
+
# here depends on when the gateway was forked relative to this shell.
|
|
2546
|
+
#
|
|
2547
|
+
# So publish this process's identity for the gateway to probe:
|
|
2548
|
+
# pid — `$$`, and because the `exec` below REPLACES this shell rather
|
|
2549
|
+
# than forking, that pid IS the claude process's pid.
|
|
2550
|
+
# starttime — `/proc/<pid>/stat` field 22, clock ticks since host boot.
|
|
2551
|
+
# Assigned at fork and NOT reset by `exec`, so the value read
|
|
2552
|
+
# here is exactly what claude will carry. PIDs are reused (and a
|
|
2553
|
+
# container restart resets the pid namespace), so identity is the
|
|
2554
|
+
# PAIR — the gateway rejects a pid whose starttime disagrees.
|
|
2555
|
+
#
|
|
2556
|
+
# Deliberately NOT recorded: `comm`. It is "bash" here and becomes claude's
|
|
2557
|
+
# after the exec, so recording it would guarantee a mismatch. (An earlier
|
|
2558
|
+
# review suggested recording it and requiring the live process to look
|
|
2559
|
+
# claude-ish, to close a fresh-boot race cheaply — the generation token closes
|
|
2560
|
+
# that race outright, so the extra guard would only add a way to fail.)
|
|
2561
|
+
#
|
|
2562
|
+
# Written LAST, immediately before the exec: a record must never name a
|
|
2563
|
+
# process that does not exist yet. Best-effort throughout — a missing or torn
|
|
2564
|
+
# record makes the gateway fail open to its pre-#4641 behaviour.
|
|
2565
|
+
_sr_agent_stat=$(cat "/proc/$$/stat" 2>/dev/null || true)
|
|
2566
|
+
if [ -n "$_sr_agent_stat" ]; then
|
|
2567
|
+
# comm is parenthesised and may contain spaces/`)`, so split on the LAST ") ".
|
|
2568
|
+
_sr_agent_rest=${_sr_agent_stat##*') '}
|
|
2569
|
+
# Field 22 (starttime) is the 20th field after comm. Subshell + `set -f` so
|
|
2570
|
+
# the word split neither globs nor clobbers this script's positional params.
|
|
2571
|
+
_sr_agent_start=$( set -f; set -- $_sr_agent_rest; printf '%s' "${20}" )
|
|
2572
|
+
case "$_sr_agent_start" in
|
|
2573
|
+
''|*[!0-9]*) _sr_agent_start="" ;;
|
|
2574
|
+
esac
|
|
2575
|
+
if [ -n "$_sr_agent_start" ]; then
|
|
2576
|
+
_sr_agent_dir="${TELEGRAM_STATE_DIR:-{{agentDir}}/telegram}"
|
|
2577
|
+
mkdir -p "$_sr_agent_dir" 2>/dev/null || true
|
|
2578
|
+
_sr_agent_tmp="$_sr_agent_dir/agent-process.json.tmp.$$"
|
|
2579
|
+
if printf '{"pid":%s,"starttime":"%s","boot_at":%s}\n' \
|
|
2580
|
+
"$$" "$_sr_agent_start" "$(( $(date +%s) * 1000 ))" > "$_sr_agent_tmp" 2>/dev/null; then
|
|
2581
|
+
mv -f "$_sr_agent_tmp" "$_sr_agent_dir/agent-process.json" 2>/dev/null || true
|
|
2582
|
+
fi
|
|
2583
|
+
rm -f "$_sr_agent_tmp" 2>/dev/null || true
|
|
2584
|
+
# Passive context for the agent process itself (diagnostics only — the
|
|
2585
|
+
# gateway reads the FILE; it was already running when this env was set).
|
|
2586
|
+
export SWITCHROOM_AGENT_PID="$$"
|
|
2587
|
+
export SWITCHROOM_AGENT_STARTTIME="$_sr_agent_start"
|
|
2588
|
+
unset _sr_agent_dir _sr_agent_tmp
|
|
2589
|
+
fi
|
|
2590
|
+
fi
|
|
2591
|
+
unset _sr_agent_stat _sr_agent_rest _sr_agent_start
|
|
2592
|
+
|
|
2478
2593
|
{{#if useSwitchroomPlugin}}
|
|
2479
2594
|
if [ -n "$APPEND_PROMPT" ]; then
|
|
2480
2595
|
exec claude $CONTINUE_FLAG --dangerously-load-development-channels server:switchroom-telegram --plugin-dir "{{securityPluginDir}}"{{#if hindsightEnabled}} --plugin-dir "{{agentDir}}/.claude/plugins/hindsight-memory"{{/if}} $SR_FLEET_ARG --model "$_EFFECTIVE_MODEL" $_EFFORT_ARG{{#if permissionMode}} --permission-mode {{permissionMode}}{{/if}}{{#if fallbackModelQ}} --fallback-model {{{fallbackModelQ}}}{{/if}} --append-system-prompt "$APPEND_PROMPT"{{#if dangerousMode}} --dangerously-skip-permissions{{/if}}{{#if extraCliArgs}}{{{extraCliArgs}}}{{/if}}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## Local time — never show a human a UTC timestamp
|
|
2
|
+
|
|
3
|
+
Convert to the agent's zone (`SWITCHROOM_TIMEZONE`, else `TZ`; the container clock is already local).
|
|
4
|
+
|
|
5
|
+
- **Never hardcode an offset or a zone abbreviation** (`+10:00`, `AEST`) — both move with DST; derive from the zone.
|
|
6
|
+
- **Attach a naive timestamp's true zone before converting** (`.replace(tzinfo=ZoneInfo("UTC"))`) — `.astimezone()` on a naive value assumes the process clock.
|
|
@@ -16,9 +16,7 @@ You are operating in the **{{topicName}}** {{#if topicEmoji}}{{topicEmoji}} {{/i
|
|
|
16
16
|
{{/if}}
|
|
17
17
|
|
|
18
18
|
## Core Behavior
|
|
19
|
-
- Respond helpfully, concisely, and conversationally.
|
|
20
19
|
- Use your available tools when they add clear value — don't force tool use when a plain answer suffices.
|
|
21
|
-
- Save important facts, preferences, and decisions to memory so you can recall them later.
|
|
22
20
|
- When asked to do something ambiguous, ask one clarifying question rather than guessing.
|
|
23
21
|
- If a task has multiple steps, outline your plan before executing.
|
|
24
22
|
|
|
@@ -58,7 +56,6 @@ Hard rules the agent must follow during reflect — guardrails that are always a
|
|
|
58
56
|
|
|
59
57
|
Retain proactively when:
|
|
60
58
|
- The user shares a preference or fact about themselves
|
|
61
|
-
- The user gives you a correction or rule (these go to directives, not retain)
|
|
62
59
|
- A significant decision was made and the rationale matters for next time
|
|
63
60
|
- You did real work and the result + the path you took would be useful next session
|
|
64
61
|
|
|
@@ -68,8 +65,6 @@ Don't retain:
|
|
|
68
65
|
- Sensitive content the user explicitly asked you to not remember
|
|
69
66
|
- Things already in a mental model — they'll be re-derived from underlying memories
|
|
70
67
|
|
|
71
|
-
Auto-retain (see above) covers routine capture; use manual `retain` for high-signal observations you want immediately searchable.
|
|
72
|
-
|
|
73
68
|
### When to synthesize — concrete triggers
|
|
74
69
|
|
|
75
70
|
Auto-recall and auto-retain feed the bank but never *synthesize* — that's on you, only if you act on these triggers. Each has a backstop:
|
|
@@ -134,13 +129,6 @@ Discipline (you read peers' attacker-influenced output, nothing taps your shell)
|
|
|
134
129
|
Your transcript is this power's audit trail; keep your actions legible.
|
|
135
130
|
{{/if}}
|
|
136
131
|
|
|
137
|
-
## Tools
|
|
138
|
-
{{#if tools}}
|
|
139
|
-
Use the tools available to you to accomplish tasks effectively. Prefer the simplest tool that gets the job done.
|
|
140
|
-
{{else}}
|
|
141
|
-
Use your available tools when appropriate. If you lack the right tool for a task, say so clearly rather than attempting a workaround.
|
|
142
|
-
{{/if}}
|
|
143
|
-
|
|
144
132
|
{{#if schedule}}
|
|
145
133
|
## Scheduled Tasks
|
|
146
134
|
You have scheduled tasks configured. At fire time an in-container scheduler sidecar injects a synthesized inbound turn into **your running session** — a scheduled task arrives as an ordinary turn tagged `<channel source="cron">`, using your normal session, context, and model, and it shows up in your transcript and Hindsight memory like any other turn (it is *not* an isolated one-shot `claude -p` process). They survive reboots via the container restart policy plus an at-least-once boot replay.
|