switchroom 0.18.9 → 0.18.11
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 +1 -0
- package/dist/auth-broker/index.js +198 -13
- package/dist/cli/notion-write-pretool.mjs +1 -0
- package/dist/cli/switchroom.js +214 -83
- package/dist/cli/ui/apple-touch-icon.png +0 -0
- package/dist/cli/ui/favicon-32.png +0 -0
- package/dist/cli/ui/favicon.ico +0 -0
- package/dist/host-control/main.js +760 -257
- package/dist/vault/approvals/kernel-server.js +3 -1
- package/dist/vault/broker/server.js +3 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +119 -37
- package/profiles/_shared/dev-protocol.md.hbs +42 -0
- package/skills/dev-protocol/SKILL.md +131 -0
- package/telegram-plugin/README.md +2 -1
- package/telegram-plugin/admin-commands/dispatch.test.ts +40 -2
- package/telegram-plugin/admin-commands/index.ts +6 -1
- package/telegram-plugin/bridge/bridge.ts +23 -1
- package/telegram-plugin/bridge/crash-breadcrumb.ts +42 -0
- package/telegram-plugin/chat-lock.ts +13 -0
- package/telegram-plugin/dist/bridge/bridge.js +24 -1
- package/telegram-plugin/dist/gateway/gateway.js +1832 -263
- package/telegram-plugin/dist/server.js +29 -2
- package/telegram-plugin/fallback-card-collapse.ts +131 -0
- package/telegram-plugin/gateway/bridge-dead-watchdog.ts +546 -0
- package/telegram-plugin/gateway/effort-command.ts +47 -3
- package/telegram-plugin/gateway/gateway.ts +1435 -211
- package/telegram-plugin/gateway/model-command.ts +94 -8
- package/telegram-plugin/gateway/pending-session-command.ts +365 -0
- package/telegram-plugin/gateway/permission-timeout.ts +25 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +23 -3
- package/telegram-plugin/gateway/session-model-file.ts +166 -23
- package/telegram-plugin/gateway/stop-command.ts +56 -0
- package/telegram-plugin/photo-precheck.ts +201 -0
- package/telegram-plugin/quota-watch.ts +141 -2
- package/telegram-plugin/registry/subagents-schema.ts +26 -3
- package/telegram-plugin/registry/subagents.test.ts +67 -0
- package/telegram-plugin/retry-api-call.ts +31 -0
- package/telegram-plugin/subagent-watcher.ts +392 -1
- package/telegram-plugin/tests/bridge-dead-watchdog.test.ts +576 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +11 -5
- package/telegram-plugin/tests/chat-lock-unhandled-rejection.test.ts +101 -0
- package/telegram-plugin/tests/crash-breadcrumb.test.ts +57 -0
- package/telegram-plugin/tests/effort-command.test.ts +59 -2
- package/telegram-plugin/tests/fallback-card-collapse.test.ts +104 -0
- package/telegram-plugin/tests/gateway-pending-command-wiring.test.ts +124 -0
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +7 -1
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +19 -11
- package/telegram-plugin/tests/model-command.test.ts +46 -3
- package/telegram-plugin/tests/pending-session-command.test.ts +322 -0
- package/telegram-plugin/tests/permission-timeout.test.ts +26 -0
- package/telegram-plugin/tests/permission-verdict-resume-guard.test.ts +16 -0
- package/telegram-plugin/tests/photo-dimension-fallback.test.ts +129 -0
- package/telegram-plugin/tests/photo-precheck.test.ts +240 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +85 -0
- package/telegram-plugin/tests/quota-watch.test.ts +225 -0
- package/telegram-plugin/tests/session-model-file.test.ts +101 -2
- package/telegram-plugin/tests/stop-command.test.ts +234 -0
- package/telegram-plugin/tests/subagent-watcher-env-thresholds.test.ts +27 -9
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +398 -0
- package/telegram-plugin/tests/subagent-watcher-stall-terminal.test.ts +172 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +37 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +18 -4
- package/telegram-plugin/welcome-text.ts +4 -3
- package/telegram-plugin/worker-activity-feed.ts +27 -0
|
@@ -9966,7 +9966,7 @@ var require_util = __commonJS((exports) => {
|
|
|
9966
9966
|
return path;
|
|
9967
9967
|
}
|
|
9968
9968
|
exports.normalize = normalize;
|
|
9969
|
-
function
|
|
9969
|
+
function join3(aRoot, aPath) {
|
|
9970
9970
|
if (aRoot === "") {
|
|
9971
9971
|
aRoot = ".";
|
|
9972
9972
|
}
|
|
@@ -9998,7 +9998,7 @@ var require_util = __commonJS((exports) => {
|
|
|
9998
9998
|
}
|
|
9999
9999
|
return joined;
|
|
10000
10000
|
}
|
|
10001
|
-
exports.join =
|
|
10001
|
+
exports.join = join3;
|
|
10002
10002
|
exports.isAbsolute = function(aPath) {
|
|
10003
10003
|
return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
|
|
10004
10004
|
};
|
|
@@ -10171,7 +10171,7 @@ var require_util = __commonJS((exports) => {
|
|
|
10171
10171
|
parsed.path = parsed.path.substring(0, index + 1);
|
|
10172
10172
|
}
|
|
10173
10173
|
}
|
|
10174
|
-
sourceURL =
|
|
10174
|
+
sourceURL = join3(urlGenerate(parsed), sourceURL);
|
|
10175
10175
|
}
|
|
10176
10176
|
return normalize(sourceURL);
|
|
10177
10177
|
}
|
|
@@ -15274,7 +15274,7 @@ var init_secretlint_source = __esm(() => {
|
|
|
15274
15274
|
});
|
|
15275
15275
|
|
|
15276
15276
|
// src/util/atomic.ts
|
|
15277
|
-
import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync, rmSync, writeSync } from "node:fs";
|
|
15277
|
+
import { closeSync as closeSync2, constants, fsyncSync, openSync as openSync2, renameSync as renameSync2, rmSync, writeSync } from "node:fs";
|
|
15278
15278
|
var TMP_OPEN_FLAGS;
|
|
15279
15279
|
var init_atomic = __esm(() => {
|
|
15280
15280
|
TMP_OPEN_FLAGS = constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0);
|
|
@@ -17316,8 +17316,8 @@ var require_commander = __commonJS((exports2) => {
|
|
|
17316
17316
|
|
|
17317
17317
|
// src/host-control/main.ts
|
|
17318
17318
|
import { homedir as homedir6 } from "node:os";
|
|
17319
|
-
import { existsSync as
|
|
17320
|
-
import { join as
|
|
17319
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4 } from "node:fs";
|
|
17320
|
+
import { join as join11, resolve as resolve9 } from "node:path";
|
|
17321
17321
|
|
|
17322
17322
|
// src/config/loader.ts
|
|
17323
17323
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
@@ -21986,6 +21986,7 @@ var SwitchroomConfigSchema = exports_external.object({
|
|
|
21986
21986
|
uid: exports_external.number().int().nonnegative().optional().describe("Optional UID to chown the consumer socket to (defaults to 0 = root, " + "suitable for sibling containers running as root)."),
|
|
21987
21987
|
mirror_dir: exports_external.string().optional().describe("Optional host-side directory path. When set, the broker actively " + "writes the consumer's effective-account `.credentials.json` mirror " + "here — in addition to serving creds on demand via `get-credentials`. " + "Use this to eliminate the pull-latency gap: without a mirror the " + "consumer only gets failover creds at its next scheduled re-fetch " + "(up to 30 min). With a mirror the broker pushes failover creds " + "immediately when it detects exhaustion (consumer-quota-sensor tick, " + "or a mark-exhausted RPC on the pinned account). The directory must " + "be accessible to the broker container (bind-mounted from the host) " + "and to the consumer container; the broker writes " + "`<mirror_dir>/.credentials.json` atomically. Chown is attempted to " + "`uid` (default 0) — swallowed when CAP_CHOWN is absent.")
|
|
21988
21988
|
})).optional().describe("Non-agent peers that hold a broker socket (RFC H §4.8). Each gets " + "its own `/run/switchroom/auth-broker/<name>/sock` chowned to its UID. " + "Consumers cannot be admins; a consumer name that collides with an " + "agent (whether that agent has `admin: true` or not) is a config " + "error caught at schema validation."),
|
|
21989
|
+
proactive_failover_pct: exports_external.number().min(1).max(99).optional().describe("Soft-avoid threshold (percent) for proactive auth failover (#3031). " + "When set (e.g. 95), an account whose fresh 7-day utilization is at/" + "above this value — or whose 5h utilization is at/above min(pct+3, " + "98) — enters the SOFT-AVOID preference tier: the broker prefers a " + "fully-eligible fallback account on the serving path, without ever " + "blocking the account (the hard wall stays 99.5). Enter/exit uses " + "hysteresis (exit below pct-5 or on window reset) so the preference " + "does not flap between probe ticks. Accounts serving past the wall " + "via `allow_overage_accounts` are never soft-avoided. UNSET (the " + "default) disables the tier entirely — behavior is identical to a " + "fleet without this field."),
|
|
21989
21990
|
allow_overage_accounts: exports_external.array(exports_external.string().min(1)).optional().describe("Opt-in list of account labels (bare strings matching `auth.active` / " + "`auth.fallback_order` entries) that may be served PAST the weekly " + "utilization wall when Anthropic overage billing is available for the " + "account (`overageStatus === 'allowed'`). Overage is REAL MONEY — " + "default is empty (no account gets this). An account in this list is " + "only kept eligible when its fresh quota snapshot reports " + "`overageStatus: 'allowed'` AND `overageDisabledReason` is NOT " + "'out_of_credits' (i.e. the overage credit has not been exhausted). " + "As soon as `overageDisabledReason` becomes 'out_of_credits', the " + "account is blocked immediately regardless of this flag. Overage lifts " + "ONLY the utilization wall — it cannot lift an active exhaustion mark " + "written by a real 429 (`mark-exhausted`).")
|
|
21990
21991
|
}).optional().describe("Switchroom-auth-broker configuration (RFC H). Fleet-wide active account, " + "fallback order, admin-agent ACL, and ephemeral-consumer surface. " + "Required from the v0.8+ schema onwards; pre-v0.8 fleets are migrated " + "in-place by `switchroom apply` (see src/auth/migrate-schema.ts)."),
|
|
21991
21992
|
drive: GoogleWorkspaceConfigSchema.describe("RFC D legacy key — use `google_workspace:` instead. Optional Google " + "Workspace onboarding configuration. When set, supplies Google OAuth " + "client credentials, the approver allowlist for `switchroom drive " + "connect`, and the optional tier knob. Env vars " + "(SWITCHROOM_GOOGLE_CLIENT_ID, SWITCHROOM_GOOGLE_CLIENT_SECRET, " + "SWITCHROOM_APPROVER_USER_ID) take precedence over this block when " + "set, preserving back-compat with the env-only flow shipped in #766."),
|
|
@@ -22796,11 +22797,391 @@ function resolveAgentsDir(config) {
|
|
|
22796
22797
|
return resolveDualPath(config.switchroom.agents_dir || "~/.switchroom/agents");
|
|
22797
22798
|
}
|
|
22798
22799
|
|
|
22800
|
+
// src/host-control/config-degraded.ts
|
|
22801
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, renameSync, unlinkSync, watchFile, unwatchFile, writeFileSync, mkdirSync } from "node:fs";
|
|
22802
|
+
import { connect } from "node:net";
|
|
22803
|
+
import { join } from "node:path";
|
|
22804
|
+
|
|
22805
|
+
// src/config/release-resolve.ts
|
|
22806
|
+
function parseSemverTag(tag) {
|
|
22807
|
+
if (!tag)
|
|
22808
|
+
return null;
|
|
22809
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag.trim());
|
|
22810
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
22811
|
+
}
|
|
22812
|
+
function compareReleaseTags(a, b) {
|
|
22813
|
+
const pa = parseSemverTag(a);
|
|
22814
|
+
const pb = parseSemverTag(b);
|
|
22815
|
+
if (!pa || !pb)
|
|
22816
|
+
return null;
|
|
22817
|
+
for (let i = 0;i < 3; i++) {
|
|
22818
|
+
if (pa[i] !== pb[i])
|
|
22819
|
+
return pa[i] < pb[i] ? -1 : 1;
|
|
22820
|
+
}
|
|
22821
|
+
return 0;
|
|
22822
|
+
}
|
|
22823
|
+
|
|
22824
|
+
// src/host-control/self-bump.ts
|
|
22825
|
+
var SELF_BUMP_MARKER_FILENAME = "pending-rollout.json";
|
|
22826
|
+
var SELF_BUMP_HELPER_CONTAINER = "switchroom-hostd-selfbump";
|
|
22827
|
+
var SELF_BUMP_LOG_FILENAME = "selfbump.log";
|
|
22828
|
+
var SELF_BUMP_MARKER_MAX_AGE_MS = 15 * 60 * 1000;
|
|
22829
|
+
var SEMVER_PIN_RE = /^v\d+\.\d+\.\d+$/;
|
|
22830
|
+
function needsSelfBump(ownVersion, pin) {
|
|
22831
|
+
if (!ownVersion)
|
|
22832
|
+
return false;
|
|
22833
|
+
const cmp = compareReleaseTags(`v${ownVersion.trim().replace(/^v/, "")}`, `v${pin.trim().replace(/^v/, "")}`);
|
|
22834
|
+
return cmp !== null && cmp < 0;
|
|
22835
|
+
}
|
|
22836
|
+
function bumpHostdComposeImageTag(yaml, pin) {
|
|
22837
|
+
const re = /^(\s*image:\s*)(\S*switchroom-hostd):(\S+)\s*$/m;
|
|
22838
|
+
const m = yaml.match(re);
|
|
22839
|
+
if (!m)
|
|
22840
|
+
return null;
|
|
22841
|
+
const oldImageRef = `${m[2]}:${m[3]}`;
|
|
22842
|
+
const newImageRef = `${m[2]}:${pin}`;
|
|
22843
|
+
return {
|
|
22844
|
+
yaml: yaml.replace(re, `$1${newImageRef}`),
|
|
22845
|
+
oldImageRef,
|
|
22846
|
+
newImageRef
|
|
22847
|
+
};
|
|
22848
|
+
}
|
|
22849
|
+
function encodePendingRolloutMarker(m) {
|
|
22850
|
+
return JSON.stringify(m, null, 2) + `
|
|
22851
|
+
`;
|
|
22852
|
+
}
|
|
22853
|
+
function parsePendingRolloutMarker(raw) {
|
|
22854
|
+
let obj;
|
|
22855
|
+
try {
|
|
22856
|
+
obj = JSON.parse(raw);
|
|
22857
|
+
} catch {
|
|
22858
|
+
return null;
|
|
22859
|
+
}
|
|
22860
|
+
if (typeof obj !== "object" || obj === null)
|
|
22861
|
+
return null;
|
|
22862
|
+
const o = obj;
|
|
22863
|
+
if (o.v !== 1)
|
|
22864
|
+
return null;
|
|
22865
|
+
if (typeof o.request_id !== "string" || o.request_id.length === 0)
|
|
22866
|
+
return null;
|
|
22867
|
+
if (typeof o.pin !== "string" || !SEMVER_PIN_RE.test(o.pin))
|
|
22868
|
+
return null;
|
|
22869
|
+
if (typeof o.created_at !== "string" || !Number.isFinite(Date.parse(o.created_at))) {
|
|
22870
|
+
return null;
|
|
22871
|
+
}
|
|
22872
|
+
if (typeof o.prior_hostd_version !== "string")
|
|
22873
|
+
return null;
|
|
22874
|
+
const caller = o.caller;
|
|
22875
|
+
if (typeof caller !== "object" || caller === null)
|
|
22876
|
+
return null;
|
|
22877
|
+
if (caller.kind === "agent") {
|
|
22878
|
+
if (typeof caller.name !== "string" || caller.name.length === 0)
|
|
22879
|
+
return null;
|
|
22880
|
+
} else if (caller.kind !== "operator") {
|
|
22881
|
+
return null;
|
|
22882
|
+
}
|
|
22883
|
+
if (o.agents !== undefined) {
|
|
22884
|
+
if (!Array.isArray(o.agents) || o.agents.length === 0 || !o.agents.every((a) => typeof a === "string" && /^[\w-]+$/.test(a))) {
|
|
22885
|
+
return null;
|
|
22886
|
+
}
|
|
22887
|
+
}
|
|
22888
|
+
if (o.skip_web !== undefined && typeof o.skip_web !== "boolean")
|
|
22889
|
+
return null;
|
|
22890
|
+
if (o.allow_downgrade !== undefined && typeof o.allow_downgrade !== "boolean") {
|
|
22891
|
+
return null;
|
|
22892
|
+
}
|
|
22893
|
+
return {
|
|
22894
|
+
v: 1,
|
|
22895
|
+
request_id: o.request_id,
|
|
22896
|
+
pin: o.pin,
|
|
22897
|
+
...o.agents !== undefined ? { agents: o.agents } : {},
|
|
22898
|
+
...o.skip_web !== undefined ? { skip_web: o.skip_web } : {},
|
|
22899
|
+
...o.allow_downgrade !== undefined ? { allow_downgrade: o.allow_downgrade } : {},
|
|
22900
|
+
caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
|
|
22901
|
+
created_at: o.created_at,
|
|
22902
|
+
prior_hostd_version: o.prior_hostd_version
|
|
22903
|
+
};
|
|
22904
|
+
}
|
|
22905
|
+
function isMarkerFresh(marker, nowMs) {
|
|
22906
|
+
const created = Date.parse(marker.created_at);
|
|
22907
|
+
if (!Number.isFinite(created))
|
|
22908
|
+
return false;
|
|
22909
|
+
return nowMs - created < SELF_BUMP_MARKER_MAX_AGE_MS;
|
|
22910
|
+
}
|
|
22911
|
+
function selfBumpHelperArgs(opts) {
|
|
22912
|
+
const composePath = `${opts.hostdDirHostPath}/docker-compose.yml`;
|
|
22913
|
+
const logPath = `${opts.hostdDirHostPath}/${SELF_BUMP_LOG_FILENAME}`;
|
|
22914
|
+
const script = `sleep 2; ` + `echo "selfbump $(date -u) -> ${opts.targetImage}" >> "${logPath}" 2>&1; ` + `docker compose -p switchroom-hostd -f "${composePath}" pull >> "${logPath}" 2>&1 && ` + `docker compose -p switchroom-hostd -f "${composePath}" up -d >> "${logPath}" 2>&1; ` + `rc=$?; echo "selfbump done rc=$rc" >> "${logPath}" 2>&1; exit $rc`;
|
|
22915
|
+
return [
|
|
22916
|
+
"run",
|
|
22917
|
+
"-d",
|
|
22918
|
+
"--rm",
|
|
22919
|
+
"--name",
|
|
22920
|
+
SELF_BUMP_HELPER_CONTAINER,
|
|
22921
|
+
"--label",
|
|
22922
|
+
"switchroom.hostd-selfbump=true",
|
|
22923
|
+
"-v",
|
|
22924
|
+
"/var/run/docker.sock:/var/run/docker.sock",
|
|
22925
|
+
"-v",
|
|
22926
|
+
`${opts.hostdDirHostPath}:${opts.hostdDirHostPath}`,
|
|
22927
|
+
"--entrypoint",
|
|
22928
|
+
"/bin/sh",
|
|
22929
|
+
opts.helperImage,
|
|
22930
|
+
"-c",
|
|
22931
|
+
script
|
|
22932
|
+
];
|
|
22933
|
+
}
|
|
22934
|
+
|
|
22935
|
+
// src/host-control/config-degraded.ts
|
|
22936
|
+
var CONFIG_DEGRADED_MARKER_FILENAME = "config-degraded.json";
|
|
22937
|
+
var CONFIG_DEGRADED_RETRY_MS = 15000;
|
|
22938
|
+
var MAX_PRESERVED_DEGRADED_MS = 4 * 60 * 60 * 1000;
|
|
22939
|
+
function buildDegradedMarker(err, nowMs) {
|
|
22940
|
+
return {
|
|
22941
|
+
v: 1,
|
|
22942
|
+
since: new Date(nowMs).toISOString(),
|
|
22943
|
+
error: err.message,
|
|
22944
|
+
...err.details && err.details.length > 0 ? { details: err.details } : {}
|
|
22945
|
+
};
|
|
22946
|
+
}
|
|
22947
|
+
function readDegradedSince(markerPath) {
|
|
22948
|
+
try {
|
|
22949
|
+
if (!existsSync4(markerPath))
|
|
22950
|
+
return null;
|
|
22951
|
+
const parsed = JSON.parse(readFileSync3(markerPath, "utf8"));
|
|
22952
|
+
if (parsed?.v !== 1 || typeof parsed.since !== "string")
|
|
22953
|
+
return null;
|
|
22954
|
+
const since = Date.parse(parsed.since);
|
|
22955
|
+
return Number.isFinite(since) ? since : null;
|
|
22956
|
+
} catch {
|
|
22957
|
+
return null;
|
|
22958
|
+
}
|
|
22959
|
+
}
|
|
22960
|
+
function formatDegradedNotice(err) {
|
|
22961
|
+
const details = (err.details ?? []).map((d) => d.trim()).filter(Boolean).slice(0, 6).join(`
|
|
22962
|
+
`);
|
|
22963
|
+
return `⚠️ hostd: switchroom.yaml failed to load — running DEGRADED.
|
|
22964
|
+
` + `${err.message}
|
|
22965
|
+
` + (details ? details + `
|
|
22966
|
+
` : "") + `Retrying every ${Math.round(CONFIG_DEGRADED_RETRY_MS / 1000)}s and on file change. ` + "hostd's sockets are not bound yet, so fleet operations (rollouts, config " + "edits, restarts) will fail with connection errors until the config parses. " + "Fix the YAML (`switchroom config check` validates it) — no hostd restart " + "needed. A pending self-bump rollout, if any, will resume once the config " + "loads (its 15-min freshness budget is frozen while degraded, up to " + `${Math.round(MAX_PRESERVED_DEGRADED_MS / 3600000)}h).`;
|
|
22967
|
+
}
|
|
22968
|
+
function humanMins(ms) {
|
|
22969
|
+
const mins = Math.max(1, Math.round(ms / 60000));
|
|
22970
|
+
return mins >= 90 ? `${(mins / 60).toFixed(1)}h` : `${mins}m`;
|
|
22971
|
+
}
|
|
22972
|
+
function formatRecoveredNotice(degradedMs, pending) {
|
|
22973
|
+
let tail = "";
|
|
22974
|
+
if (pending?.preserved) {
|
|
22975
|
+
tail = ` Pending self-bump rollout to ${pending.pin} was preserved through the ` + `outage (marker now ${humanMins(pending.ageMs)} old) and will resume now.`;
|
|
22976
|
+
} else if (pending) {
|
|
22977
|
+
tail = ` NOTE: pending self-bump rollout to ${pending.pin} was NOT auto-resumed — ` + `degraded for ~${humanMins(degradedMs)}, past the ` + `${Math.round(MAX_PRESERVED_DEGRADED_MS / 3600000)}h preservation cap. Its ` + `marker (${humanMins(pending.ageMs)} old) will be treated as stale; re-run ` + `the rollout if it is still wanted.`;
|
|
22978
|
+
}
|
|
22979
|
+
return `✅ hostd: switchroom.yaml loads again after ~${humanMins(degradedMs)} degraded — ` + `normal service resumed.` + tail;
|
|
22980
|
+
}
|
|
22981
|
+
function shiftPendingRolloutMarker(raw, degradedMs) {
|
|
22982
|
+
if (degradedMs <= 0)
|
|
22983
|
+
return null;
|
|
22984
|
+
const marker = parsePendingRolloutMarker(raw);
|
|
22985
|
+
if (!marker)
|
|
22986
|
+
return null;
|
|
22987
|
+
const created = Date.parse(marker.created_at);
|
|
22988
|
+
if (!Number.isFinite(created))
|
|
22989
|
+
return null;
|
|
22990
|
+
return encodePendingRolloutMarker({
|
|
22991
|
+
...marker,
|
|
22992
|
+
created_at: new Date(created + degradedMs).toISOString()
|
|
22993
|
+
});
|
|
22994
|
+
}
|
|
22995
|
+
function clearStaleDegradedMarker(homeDir, log) {
|
|
22996
|
+
const markerPath = join(homeDir, ".switchroom", "hostd", CONFIG_DEGRADED_MARKER_FILENAME);
|
|
22997
|
+
try {
|
|
22998
|
+
if (!existsSync4(markerPath))
|
|
22999
|
+
return false;
|
|
23000
|
+
unlinkSync(markerPath);
|
|
23001
|
+
log(`config-degraded: clearing stale degraded marker from a prior crash ` + `(${markerPath}) — config loads cleanly on this boot`);
|
|
23002
|
+
return true;
|
|
23003
|
+
} catch (e) {
|
|
23004
|
+
log(`config-degraded: could not clear stale marker ${markerPath}: ${e.message}`);
|
|
23005
|
+
return false;
|
|
23006
|
+
}
|
|
23007
|
+
}
|
|
23008
|
+
function postOperatorNoticeViaGateways(candidates, text, log, connectTimeoutMs = 5000) {
|
|
23009
|
+
const tryOne = (c) => new Promise((resolve4) => {
|
|
23010
|
+
let done = false;
|
|
23011
|
+
const finish = (ok, why) => {
|
|
23012
|
+
if (done)
|
|
23013
|
+
return;
|
|
23014
|
+
done = true;
|
|
23015
|
+
if (why)
|
|
23016
|
+
log(`config-degraded: gateway ${c.agent} — ${why}`);
|
|
23017
|
+
try {
|
|
23018
|
+
client.destroy();
|
|
23019
|
+
} catch {}
|
|
23020
|
+
resolve4(ok);
|
|
23021
|
+
};
|
|
23022
|
+
let client;
|
|
23023
|
+
try {
|
|
23024
|
+
client = connect({ path: c.sock });
|
|
23025
|
+
} catch (e) {
|
|
23026
|
+
log(`config-degraded: gateway ${c.agent} — connect threw: ${e.message}`);
|
|
23027
|
+
resolve4(false);
|
|
23028
|
+
return;
|
|
23029
|
+
}
|
|
23030
|
+
client.setTimeout(connectTimeoutMs);
|
|
23031
|
+
client.on("connect", () => {
|
|
23032
|
+
try {
|
|
23033
|
+
client.write(JSON.stringify({
|
|
23034
|
+
type: "rollout_status_post",
|
|
23035
|
+
requestId: "config-degraded",
|
|
23036
|
+
agentName: c.agent,
|
|
23037
|
+
text
|
|
23038
|
+
}) + `
|
|
23039
|
+
`, () => {
|
|
23040
|
+
client.end();
|
|
23041
|
+
finish(true);
|
|
23042
|
+
});
|
|
23043
|
+
} catch (e) {
|
|
23044
|
+
finish(false, `write failed: ${e.message}`);
|
|
23045
|
+
}
|
|
23046
|
+
});
|
|
23047
|
+
client.on("timeout", () => finish(false, "connect timed out"));
|
|
23048
|
+
client.on("error", (e) => finish(false, `socket error: ${e.message}`));
|
|
23049
|
+
});
|
|
23050
|
+
return (async () => {
|
|
23051
|
+
for (const c of candidates) {
|
|
23052
|
+
if (await tryOne(c))
|
|
23053
|
+
return c.agent;
|
|
23054
|
+
}
|
|
23055
|
+
if (candidates.length > 0) {
|
|
23056
|
+
log("config-degraded: every candidate gateway socket failed; relying on marker file");
|
|
23057
|
+
}
|
|
23058
|
+
return null;
|
|
23059
|
+
})();
|
|
23060
|
+
}
|
|
23061
|
+
async function waitForConfigRecovery(opts) {
|
|
23062
|
+
const now = opts.nowFn ?? Date.now;
|
|
23063
|
+
const retryMs = opts.retryMs ?? CONFIG_DEGRADED_RETRY_MS;
|
|
23064
|
+
const hostdDir = join(opts.homeDir, ".switchroom", "hostd");
|
|
23065
|
+
const degradedMarkerPath = join(hostdDir, CONFIG_DEGRADED_MARKER_FILENAME);
|
|
23066
|
+
const pendingMarkerPath = join(hostdDir, SELF_BUMP_MARKER_FILENAME);
|
|
23067
|
+
const persistedSince = readDegradedSince(degradedMarkerPath);
|
|
23068
|
+
const enteredAt = Math.min(persistedSince ?? now(), now());
|
|
23069
|
+
if (persistedSince !== null) {
|
|
23070
|
+
opts.log(`config-degraded: resuming an existing degraded window (since ` + `${new Date(enteredAt).toISOString()}, persisted in ${degradedMarkerPath})`);
|
|
23071
|
+
}
|
|
23072
|
+
try {
|
|
23073
|
+
mkdirSync(hostdDir, { recursive: true, mode: 448 });
|
|
23074
|
+
writeFileSync(degradedMarkerPath + ".tmp", JSON.stringify(buildDegradedMarker(opts.initialError, enteredAt), null, 2) + `
|
|
23075
|
+
`, { mode: 384 });
|
|
23076
|
+
renameSync(degradedMarkerPath + ".tmp", degradedMarkerPath);
|
|
23077
|
+
} catch (e) {
|
|
23078
|
+
opts.log(`config-degraded: could not write marker ${degradedMarkerPath}: ${e.message}`);
|
|
23079
|
+
}
|
|
23080
|
+
try {
|
|
23081
|
+
opts.notify(formatDegradedNotice(opts.initialError));
|
|
23082
|
+
} catch (e) {
|
|
23083
|
+
opts.log(`config-degraded: notify threw: ${e.message}`);
|
|
23084
|
+
}
|
|
23085
|
+
opts.log(`config-degraded: entering degraded mode — ${opts.initialError.message}. ` + `Retrying every ${Math.round(retryMs / 1000)}s` + (opts.configPath ? ` and on change to ${opts.configPath}` : "") + `. Marker: ${degradedMarkerPath}`);
|
|
23086
|
+
const config = await new Promise((resolve4, reject) => {
|
|
23087
|
+
let timer = null;
|
|
23088
|
+
let settled = false;
|
|
23089
|
+
const cleanup = () => {
|
|
23090
|
+
if (timer)
|
|
23091
|
+
clearInterval(timer);
|
|
23092
|
+
if (opts.configPath) {
|
|
23093
|
+
try {
|
|
23094
|
+
unwatchFile(opts.configPath, onFileChange);
|
|
23095
|
+
} catch {}
|
|
23096
|
+
}
|
|
23097
|
+
};
|
|
23098
|
+
const attempt = (why) => {
|
|
23099
|
+
if (settled)
|
|
23100
|
+
return;
|
|
23101
|
+
try {
|
|
23102
|
+
const cfg = opts.loadFn();
|
|
23103
|
+
settled = true;
|
|
23104
|
+
cleanup();
|
|
23105
|
+
resolve4(cfg);
|
|
23106
|
+
} catch (err) {
|
|
23107
|
+
if (err instanceof ConfigError) {
|
|
23108
|
+
opts.log(`config-degraded: reload attempt (${why}) still failing — ${err.message}`);
|
|
23109
|
+
return;
|
|
23110
|
+
}
|
|
23111
|
+
settled = true;
|
|
23112
|
+
cleanup();
|
|
23113
|
+
reject(err);
|
|
23114
|
+
}
|
|
23115
|
+
};
|
|
23116
|
+
const onFileChange = () => attempt("file change");
|
|
23117
|
+
timer = setInterval(() => attempt("interval"), retryMs);
|
|
23118
|
+
if (opts.configPath) {
|
|
23119
|
+
try {
|
|
23120
|
+
watchFile(opts.configPath, { interval: 5000 }, onFileChange);
|
|
23121
|
+
} catch (e) {
|
|
23122
|
+
opts.log(`config-degraded: could not watch ${opts.configPath}: ${e.message}`);
|
|
23123
|
+
}
|
|
23124
|
+
}
|
|
23125
|
+
});
|
|
23126
|
+
const degradedMs = Math.max(0, now() - enteredAt);
|
|
23127
|
+
let pendingOutcome = null;
|
|
23128
|
+
try {
|
|
23129
|
+
if (existsSync4(pendingMarkerPath)) {
|
|
23130
|
+
const raw = readFileSync3(pendingMarkerPath, "utf8");
|
|
23131
|
+
const marker = parsePendingRolloutMarker(raw);
|
|
23132
|
+
if (marker && degradedMs <= 0) {
|
|
23133
|
+
pendingOutcome = {
|
|
23134
|
+
pin: marker.pin,
|
|
23135
|
+
ageMs: Math.max(0, now() - Date.parse(marker.created_at)),
|
|
23136
|
+
preserved: true
|
|
23137
|
+
};
|
|
23138
|
+
} else if (marker) {
|
|
23139
|
+
const withinCap = degradedMs <= MAX_PRESERVED_DEGRADED_MS;
|
|
23140
|
+
const shifted = withinCap ? shiftPendingRolloutMarker(raw, degradedMs) : null;
|
|
23141
|
+
if (shifted !== null) {
|
|
23142
|
+
writeFileSync(pendingMarkerPath + ".tmp", shifted, { mode: 384 });
|
|
23143
|
+
renameSync(pendingMarkerPath + ".tmp", pendingMarkerPath);
|
|
23144
|
+
pendingOutcome = {
|
|
23145
|
+
pin: marker.pin,
|
|
23146
|
+
ageMs: Math.max(0, now() - (Date.parse(marker.created_at) + degradedMs)),
|
|
23147
|
+
preserved: true
|
|
23148
|
+
};
|
|
23149
|
+
opts.log(`config-degraded: shifted pending-rollout marker (${marker.pin}) ` + `created_at by ${Math.round(degradedMs / 1000)}s so the degraded ` + `window does not count against the resume cutoff`);
|
|
23150
|
+
} else {
|
|
23151
|
+
pendingOutcome = {
|
|
23152
|
+
pin: marker.pin,
|
|
23153
|
+
ageMs: Math.max(0, now() - Date.parse(marker.created_at)),
|
|
23154
|
+
preserved: false
|
|
23155
|
+
};
|
|
23156
|
+
if (!withinCap) {
|
|
23157
|
+
opts.log(`config-degraded: NOT preserving pending-rollout marker ` + `(${marker.pin}) — degraded ${Math.round(degradedMs / 60000)}m, ` + `past the ${Math.round(MAX_PRESERVED_DEGRADED_MS / 3600000)}h ` + `preservation cap; the roll will not auto-resume`);
|
|
23158
|
+
}
|
|
23159
|
+
}
|
|
23160
|
+
}
|
|
23161
|
+
}
|
|
23162
|
+
} catch (e) {
|
|
23163
|
+
opts.log(`config-degraded: could not preserve pending-rollout marker: ${e.message}`);
|
|
23164
|
+
}
|
|
23165
|
+
try {
|
|
23166
|
+
if (existsSync4(degradedMarkerPath))
|
|
23167
|
+
unlinkSync(degradedMarkerPath);
|
|
23168
|
+
} catch (e) {
|
|
23169
|
+
opts.log(`config-degraded: could not remove marker ${degradedMarkerPath}: ${e.message}`);
|
|
23170
|
+
}
|
|
23171
|
+
try {
|
|
23172
|
+
opts.notify(formatRecoveredNotice(degradedMs, pendingOutcome));
|
|
23173
|
+
} catch (e) {
|
|
23174
|
+
opts.log(`config-degraded: recovery notify threw: ${e.message}`);
|
|
23175
|
+
}
|
|
23176
|
+
opts.log(`config-degraded: config loads again after ${Math.round(degradedMs / 1000)}s — resuming normal boot`);
|
|
23177
|
+
return config;
|
|
23178
|
+
}
|
|
23179
|
+
|
|
22799
23180
|
// src/agents/compose.ts
|
|
22800
23181
|
import { createHash } from "node:crypto";
|
|
22801
23182
|
|
|
22802
23183
|
// src/agents/scaffold.ts
|
|
22803
|
-
import { join as
|
|
23184
|
+
import { join as join5, resolve as resolve5 } from "node:path";
|
|
22804
23185
|
|
|
22805
23186
|
// src/config/timezone.ts
|
|
22806
23187
|
var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
@@ -22811,7 +23192,7 @@ var CONTAINER_DEFAULT_UTC_ZONES = new Set([
|
|
|
22811
23192
|
]);
|
|
22812
23193
|
|
|
22813
23194
|
// src/cli/agent-config.ts
|
|
22814
|
-
import { join } from "node:path";
|
|
23195
|
+
import { join as join2 } from "node:path";
|
|
22815
23196
|
import { homedir as homedir2 } from "node:os";
|
|
22816
23197
|
|
|
22817
23198
|
// src/vault/broker/acl.ts
|
|
@@ -22822,12 +23203,12 @@ var WEBKITE_VAULT_KEYS = new Set([
|
|
|
22822
23203
|
]);
|
|
22823
23204
|
|
|
22824
23205
|
// src/cli/agent-config.ts
|
|
22825
|
-
var AUDIT_ROOT =
|
|
23206
|
+
var AUDIT_ROOT = join2(homedir2(), ".switchroom", "audit");
|
|
22826
23207
|
|
|
22827
23208
|
// src/agents/profiles.ts
|
|
22828
23209
|
var import_handlebars = __toESM(require_lib(), 1);
|
|
22829
|
-
import { readFileSync as
|
|
22830
|
-
import { resolve as resolve4, join as
|
|
23210
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync as readdirSync2, statSync as statSync2, copyFileSync, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
|
|
23211
|
+
import { resolve as resolve4, join as join3, sep as pathSep } from "node:path";
|
|
22831
23212
|
var PROFILES_ROOT = resolve4(import.meta.dirname, "../../profiles");
|
|
22832
23213
|
import_handlebars.default.registerHelper("json", (value) => {
|
|
22833
23214
|
return new import_handlebars.default.SafeString(JSON.stringify(value, null, 2));
|
|
@@ -22836,11 +23217,11 @@ import_handlebars.default.registerHelper("isNumber", (value) => {
|
|
|
22836
23217
|
return typeof value === "number" && Number.isFinite(value);
|
|
22837
23218
|
});
|
|
22838
23219
|
var SHARED_FRAGMENTS_DIR = resolve4(PROFILES_ROOT, "_shared");
|
|
22839
|
-
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline"];
|
|
23220
|
+
var SHARED_FRAGMENTS = ["vault-protocol", "agent-self-service", "execution-discipline", "reply-discipline", "dev-protocol"];
|
|
22840
23221
|
for (const name of SHARED_FRAGMENTS) {
|
|
22841
|
-
const fragPath =
|
|
22842
|
-
if (
|
|
22843
|
-
import_handlebars.default.registerPartial(name,
|
|
23222
|
+
const fragPath = join3(SHARED_FRAGMENTS_DIR, `${name}.md.hbs`);
|
|
23223
|
+
if (existsSync5(fragPath)) {
|
|
23224
|
+
import_handlebars.default.registerPartial(name, readFileSync4(fragPath, "utf-8"));
|
|
22844
23225
|
}
|
|
22845
23226
|
}
|
|
22846
23227
|
|
|
@@ -22928,7 +23309,7 @@ var SCRYPT_MAXMEM = 128 * 1024 * 1024;
|
|
|
22928
23309
|
|
|
22929
23310
|
// src/vault/broker/client.ts
|
|
22930
23311
|
import { homedir as homedir3 } from "node:os";
|
|
22931
|
-
import { join as
|
|
23312
|
+
import { join as join4 } from "node:path";
|
|
22932
23313
|
|
|
22933
23314
|
// src/broker-common/peercred-path.ts
|
|
22934
23315
|
function matchSocketName(socketPath, patterns, maxNameLen) {
|
|
@@ -23278,8 +23659,8 @@ var ResponseSchema = exports_external.union([
|
|
|
23278
23659
|
]);
|
|
23279
23660
|
|
|
23280
23661
|
// src/vault/broker/client.ts
|
|
23281
|
-
var LEGACY_SOCKET_PATH =
|
|
23282
|
-
var OPERATOR_SOCKET_PATH =
|
|
23662
|
+
var LEGACY_SOCKET_PATH = join4(homedir3(), ".switchroom", "vault-broker.sock");
|
|
23663
|
+
var OPERATOR_SOCKET_PATH = join4(homedir3(), ".switchroom", "broker-operator", "sock");
|
|
23283
23664
|
|
|
23284
23665
|
// src/vault/resolver.ts
|
|
23285
23666
|
var materializedDirs = new Set;
|
|
@@ -23316,20 +23697,20 @@ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
|
|
|
23316
23697
|
import { mkdir, chmod, chown, unlink, appendFile } from "node:fs/promises";
|
|
23317
23698
|
import {
|
|
23318
23699
|
readdirSync as readdirSync3,
|
|
23319
|
-
existsSync as
|
|
23320
|
-
readFileSync as
|
|
23321
|
-
writeFileSync as
|
|
23322
|
-
renameSync as
|
|
23323
|
-
mkdirSync as
|
|
23700
|
+
existsSync as existsSync10,
|
|
23701
|
+
readFileSync as readFileSync8,
|
|
23702
|
+
writeFileSync as writeFileSync5,
|
|
23703
|
+
renameSync as renameSync3,
|
|
23704
|
+
mkdirSync as mkdirSync4,
|
|
23324
23705
|
openSync as openSync3,
|
|
23325
23706
|
ftruncateSync,
|
|
23326
23707
|
writeSync as writeSync2,
|
|
23327
23708
|
fsyncSync as fsyncSync2,
|
|
23328
23709
|
closeSync as closeSync3,
|
|
23329
23710
|
copyFileSync as copyFileSync2,
|
|
23330
|
-
unlinkSync
|
|
23711
|
+
unlinkSync as unlinkSync2
|
|
23331
23712
|
} from "node:fs";
|
|
23332
|
-
import { join as
|
|
23713
|
+
import { join as join10, dirname as dirname6, resolve as resolve8 } from "node:path";
|
|
23333
23714
|
import { createHash as createHash5, randomUUID as randomUUID2, randomBytes } from "node:crypto";
|
|
23334
23715
|
|
|
23335
23716
|
// src/host-control/protocol.ts
|
|
@@ -28018,10 +28399,10 @@ class PostHog extends PostHogBackendClient {
|
|
|
28018
28399
|
|
|
28019
28400
|
// src/analytics/posthog.ts
|
|
28020
28401
|
import {
|
|
28021
|
-
existsSync as
|
|
28022
|
-
mkdirSync as
|
|
28023
|
-
readFileSync as
|
|
28024
|
-
writeFileSync as
|
|
28402
|
+
existsSync as existsSync6,
|
|
28403
|
+
mkdirSync as mkdirSync3,
|
|
28404
|
+
readFileSync as readFileSync5,
|
|
28405
|
+
writeFileSync as writeFileSync3
|
|
28025
28406
|
} from "node:fs";
|
|
28026
28407
|
import { dirname as dirname2 } from "node:path";
|
|
28027
28408
|
import { randomUUID } from "node:crypto";
|
|
@@ -28039,8 +28420,8 @@ function getDistinctId() {
|
|
|
28039
28420
|
return cachedDistinctId;
|
|
28040
28421
|
const path = resolveStatePath("analytics-id");
|
|
28041
28422
|
try {
|
|
28042
|
-
if (
|
|
28043
|
-
const existing =
|
|
28423
|
+
if (existsSync6(path)) {
|
|
28424
|
+
const existing = readFileSync5(path, "utf-8").trim();
|
|
28044
28425
|
if (existing) {
|
|
28045
28426
|
cachedDistinctId = existing;
|
|
28046
28427
|
return existing;
|
|
@@ -28050,8 +28431,8 @@ function getDistinctId() {
|
|
|
28050
28431
|
const id = randomUUID();
|
|
28051
28432
|
cachedDistinctId = id;
|
|
28052
28433
|
try {
|
|
28053
|
-
|
|
28054
|
-
|
|
28434
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
28435
|
+
writeFileSync3(path, id, "utf-8");
|
|
28055
28436
|
} catch {}
|
|
28056
28437
|
return id;
|
|
28057
28438
|
}
|
|
@@ -28714,40 +29095,21 @@ var import_yaml3 = __toESM(require_dist(), 1);
|
|
|
28714
29095
|
// src/cli/rollout.ts
|
|
28715
29096
|
init_atomic();
|
|
28716
29097
|
|
|
28717
|
-
// src/config/release-resolve.ts
|
|
28718
|
-
function parseSemverTag(tag) {
|
|
28719
|
-
if (!tag)
|
|
28720
|
-
return null;
|
|
28721
|
-
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag.trim());
|
|
28722
|
-
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
28723
|
-
}
|
|
28724
|
-
function compareReleaseTags(a, b) {
|
|
28725
|
-
const pa = parseSemverTag(a);
|
|
28726
|
-
const pb = parseSemverTag(b);
|
|
28727
|
-
if (!pa || !pb)
|
|
28728
|
-
return null;
|
|
28729
|
-
for (let i = 0;i < 3; i++) {
|
|
28730
|
-
if (pa[i] !== pb[i])
|
|
28731
|
-
return pa[i] < pb[i] ? -1 : 1;
|
|
28732
|
-
}
|
|
28733
|
-
return 0;
|
|
28734
|
-
}
|
|
28735
|
-
|
|
28736
29098
|
// src/cli/resolve-version.ts
|
|
28737
|
-
import { existsSync as
|
|
28738
|
-
import { dirname as dirname4, join as
|
|
29099
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6 } from "node:fs";
|
|
29100
|
+
import { dirname as dirname4, join as join7 } from "node:path";
|
|
28739
29101
|
|
|
28740
29102
|
// src/build-info.ts
|
|
28741
|
-
var VERSION = "0.18.
|
|
29103
|
+
var VERSION = "0.18.11";
|
|
28742
29104
|
|
|
28743
29105
|
// src/cli/resolve-version.ts
|
|
28744
29106
|
function readPackageVersion() {
|
|
28745
29107
|
let dir = import.meta.dirname;
|
|
28746
29108
|
for (let i = 0;i < 12 && dir && dir !== "/"; i++) {
|
|
28747
|
-
const p =
|
|
28748
|
-
if (
|
|
29109
|
+
const p = join7(dir, "package.json");
|
|
29110
|
+
if (existsSync8(p)) {
|
|
28749
29111
|
try {
|
|
28750
|
-
const pkg = JSON.parse(
|
|
29112
|
+
const pkg = JSON.parse(readFileSync6(p, "utf-8"));
|
|
28751
29113
|
if (pkg?.name === "switchroom" && typeof pkg.version === "string") {
|
|
28752
29114
|
return pkg.version;
|
|
28753
29115
|
}
|
|
@@ -28894,6 +29256,7 @@ function parseRolloutPhaseLine(line) {
|
|
|
28894
29256
|
"agent-start",
|
|
28895
29257
|
"agent-done",
|
|
28896
29258
|
"persist-pin",
|
|
29259
|
+
"web-refresh",
|
|
28897
29260
|
"hostd-web-deferred",
|
|
28898
29261
|
"self-bump",
|
|
28899
29262
|
"self-bump-done"
|
|
@@ -28931,122 +29294,11 @@ function parseRolloutResultLine(stdout) {
|
|
|
28931
29294
|
return null;
|
|
28932
29295
|
}
|
|
28933
29296
|
|
|
28934
|
-
// src/host-control/self-bump.ts
|
|
28935
|
-
var SELF_BUMP_MARKER_FILENAME = "pending-rollout.json";
|
|
28936
|
-
var SELF_BUMP_HELPER_CONTAINER = "switchroom-hostd-selfbump";
|
|
28937
|
-
var SELF_BUMP_LOG_FILENAME = "selfbump.log";
|
|
28938
|
-
var SELF_BUMP_MARKER_MAX_AGE_MS = 15 * 60 * 1000;
|
|
28939
|
-
var SEMVER_PIN_RE = /^v\d+\.\d+\.\d+$/;
|
|
28940
|
-
function needsSelfBump(ownVersion, pin) {
|
|
28941
|
-
if (!ownVersion)
|
|
28942
|
-
return false;
|
|
28943
|
-
const cmp = compareReleaseTags(`v${ownVersion.trim().replace(/^v/, "")}`, `v${pin.trim().replace(/^v/, "")}`);
|
|
28944
|
-
return cmp !== null && cmp < 0;
|
|
28945
|
-
}
|
|
28946
|
-
function bumpHostdComposeImageTag(yaml, pin) {
|
|
28947
|
-
const re = /^(\s*image:\s*)(\S*switchroom-hostd):(\S+)\s*$/m;
|
|
28948
|
-
const m = yaml.match(re);
|
|
28949
|
-
if (!m)
|
|
28950
|
-
return null;
|
|
28951
|
-
const oldImageRef = `${m[2]}:${m[3]}`;
|
|
28952
|
-
const newImageRef = `${m[2]}:${pin}`;
|
|
28953
|
-
return {
|
|
28954
|
-
yaml: yaml.replace(re, `$1${newImageRef}`),
|
|
28955
|
-
oldImageRef,
|
|
28956
|
-
newImageRef
|
|
28957
|
-
};
|
|
28958
|
-
}
|
|
28959
|
-
function encodePendingRolloutMarker(m) {
|
|
28960
|
-
return JSON.stringify(m, null, 2) + `
|
|
28961
|
-
`;
|
|
28962
|
-
}
|
|
28963
|
-
function parsePendingRolloutMarker(raw) {
|
|
28964
|
-
let obj;
|
|
28965
|
-
try {
|
|
28966
|
-
obj = JSON.parse(raw);
|
|
28967
|
-
} catch {
|
|
28968
|
-
return null;
|
|
28969
|
-
}
|
|
28970
|
-
if (typeof obj !== "object" || obj === null)
|
|
28971
|
-
return null;
|
|
28972
|
-
const o = obj;
|
|
28973
|
-
if (o.v !== 1)
|
|
28974
|
-
return null;
|
|
28975
|
-
if (typeof o.request_id !== "string" || o.request_id.length === 0)
|
|
28976
|
-
return null;
|
|
28977
|
-
if (typeof o.pin !== "string" || !SEMVER_PIN_RE.test(o.pin))
|
|
28978
|
-
return null;
|
|
28979
|
-
if (typeof o.created_at !== "string" || !Number.isFinite(Date.parse(o.created_at))) {
|
|
28980
|
-
return null;
|
|
28981
|
-
}
|
|
28982
|
-
if (typeof o.prior_hostd_version !== "string")
|
|
28983
|
-
return null;
|
|
28984
|
-
const caller = o.caller;
|
|
28985
|
-
if (typeof caller !== "object" || caller === null)
|
|
28986
|
-
return null;
|
|
28987
|
-
if (caller.kind === "agent") {
|
|
28988
|
-
if (typeof caller.name !== "string" || caller.name.length === 0)
|
|
28989
|
-
return null;
|
|
28990
|
-
} else if (caller.kind !== "operator") {
|
|
28991
|
-
return null;
|
|
28992
|
-
}
|
|
28993
|
-
if (o.agents !== undefined) {
|
|
28994
|
-
if (!Array.isArray(o.agents) || o.agents.length === 0 || !o.agents.every((a) => typeof a === "string" && /^[\w-]+$/.test(a))) {
|
|
28995
|
-
return null;
|
|
28996
|
-
}
|
|
28997
|
-
}
|
|
28998
|
-
if (o.skip_web !== undefined && typeof o.skip_web !== "boolean")
|
|
28999
|
-
return null;
|
|
29000
|
-
if (o.allow_downgrade !== undefined && typeof o.allow_downgrade !== "boolean") {
|
|
29001
|
-
return null;
|
|
29002
|
-
}
|
|
29003
|
-
return {
|
|
29004
|
-
v: 1,
|
|
29005
|
-
request_id: o.request_id,
|
|
29006
|
-
pin: o.pin,
|
|
29007
|
-
...o.agents !== undefined ? { agents: o.agents } : {},
|
|
29008
|
-
...o.skip_web !== undefined ? { skip_web: o.skip_web } : {},
|
|
29009
|
-
...o.allow_downgrade !== undefined ? { allow_downgrade: o.allow_downgrade } : {},
|
|
29010
|
-
caller: caller.kind === "agent" ? { kind: "agent", name: caller.name } : { kind: "operator" },
|
|
29011
|
-
created_at: o.created_at,
|
|
29012
|
-
prior_hostd_version: o.prior_hostd_version
|
|
29013
|
-
};
|
|
29014
|
-
}
|
|
29015
|
-
function isMarkerFresh(marker, nowMs) {
|
|
29016
|
-
const created = Date.parse(marker.created_at);
|
|
29017
|
-
if (!Number.isFinite(created))
|
|
29018
|
-
return false;
|
|
29019
|
-
return nowMs - created < SELF_BUMP_MARKER_MAX_AGE_MS;
|
|
29020
|
-
}
|
|
29021
|
-
function selfBumpHelperArgs(opts) {
|
|
29022
|
-
const composePath = `${opts.hostdDirHostPath}/docker-compose.yml`;
|
|
29023
|
-
const logPath = `${opts.hostdDirHostPath}/${SELF_BUMP_LOG_FILENAME}`;
|
|
29024
|
-
const script = `sleep 2; ` + `echo "selfbump $(date -u) -> ${opts.targetImage}" >> "${logPath}" 2>&1; ` + `docker compose -p switchroom-hostd -f "${composePath}" pull >> "${logPath}" 2>&1 && ` + `docker compose -p switchroom-hostd -f "${composePath}" up -d >> "${logPath}" 2>&1; ` + `rc=$?; echo "selfbump done rc=$rc" >> "${logPath}" 2>&1; exit $rc`;
|
|
29025
|
-
return [
|
|
29026
|
-
"run",
|
|
29027
|
-
"-d",
|
|
29028
|
-
"--rm",
|
|
29029
|
-
"--name",
|
|
29030
|
-
SELF_BUMP_HELPER_CONTAINER,
|
|
29031
|
-
"--label",
|
|
29032
|
-
"switchroom.hostd-selfbump=true",
|
|
29033
|
-
"-v",
|
|
29034
|
-
"/var/run/docker.sock:/var/run/docker.sock",
|
|
29035
|
-
"-v",
|
|
29036
|
-
`${opts.hostdDirHostPath}:${opts.hostdDirHostPath}`,
|
|
29037
|
-
"--entrypoint",
|
|
29038
|
-
"/bin/sh",
|
|
29039
|
-
opts.helperImage,
|
|
29040
|
-
"-c",
|
|
29041
|
-
script
|
|
29042
|
-
];
|
|
29043
|
-
}
|
|
29044
|
-
|
|
29045
29297
|
// node_modules/.bun/commander@13.1.0/node_modules/commander/esm.mjs
|
|
29046
29298
|
var import__3 = __toESM(require_commander(), 1);
|
|
29047
29299
|
|
|
29048
29300
|
// src/cli/update.ts
|
|
29049
|
-
import { join as
|
|
29301
|
+
import { join as join8, dirname as dirname5, resolve as resolve7 } from "node:path";
|
|
29050
29302
|
import { homedir as homedir5 } from "node:os";
|
|
29051
29303
|
|
|
29052
29304
|
// src/agents/lifecycle.ts
|
|
@@ -29143,7 +29395,7 @@ function getAllAgentStatuses(config) {
|
|
|
29143
29395
|
|
|
29144
29396
|
// src/cli/update.ts
|
|
29145
29397
|
init_atomic();
|
|
29146
|
-
var DEFAULT_COMPOSE_PATH =
|
|
29398
|
+
var DEFAULT_COMPOSE_PATH = join8(homedir5(), ".switchroom", "compose", "docker-compose.yml");
|
|
29147
29399
|
var UPDATE_RESULT_SENTINEL = "SWITCHROOM_UPDATE_RESULT:";
|
|
29148
29400
|
function parseUpdateResultLine(stdout) {
|
|
29149
29401
|
const idx = stdout.lastIndexOf(UPDATE_RESULT_SENTINEL);
|
|
@@ -29166,9 +29418,9 @@ function parseUpdateResultLine(stdout) {
|
|
|
29166
29418
|
|
|
29167
29419
|
// src/host-control/config-edit-validator.ts
|
|
29168
29420
|
var import_yaml5 = __toESM(require_dist(), 1);
|
|
29169
|
-
import { mkdtempSync, writeFileSync as
|
|
29421
|
+
import { mkdtempSync, writeFileSync as writeFileSync4, rmSync as rmSync2, existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
29170
29422
|
import { tmpdir } from "node:os";
|
|
29171
|
-
import { join as
|
|
29423
|
+
import { join as join9, isAbsolute as isAbsolute2, normalize, basename as basename2 } from "node:path";
|
|
29172
29424
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
29173
29425
|
import { isDeepStrictEqual } from "node:util";
|
|
29174
29426
|
var MAX_PATCH_BYTES = 1024 * 1024;
|
|
@@ -29281,22 +29533,22 @@ function validateShape(unifiedDiff, targetPath) {
|
|
|
29281
29533
|
return null;
|
|
29282
29534
|
}
|
|
29283
29535
|
function applyPatch(unifiedDiff, configPath, gitBin) {
|
|
29284
|
-
if (!
|
|
29536
|
+
if (!existsSync9(configPath)) {
|
|
29285
29537
|
return {
|
|
29286
29538
|
ok: false,
|
|
29287
29539
|
code: "E_PATCH_APPLY_FAILED",
|
|
29288
29540
|
detail: `target config not found at ${configPath}`
|
|
29289
29541
|
};
|
|
29290
29542
|
}
|
|
29291
|
-
const liveContent =
|
|
29292
|
-
const scratchDir = mkdtempSync(
|
|
29543
|
+
const liveContent = readFileSync7(configPath, "utf8");
|
|
29544
|
+
const scratchDir = mkdtempSync(join9(tmpdir(), "config-propose-edit-"));
|
|
29293
29545
|
try {
|
|
29294
29546
|
const targetBasename = configPath.split("/").pop() ?? "switchroom.yaml";
|
|
29295
|
-
const scratchFile =
|
|
29296
|
-
|
|
29547
|
+
const scratchFile = join9(scratchDir, targetBasename);
|
|
29548
|
+
writeFileSync4(scratchFile, liveContent);
|
|
29297
29549
|
const normalizedDiff = normalizeDiffHeadersToBasename(unifiedDiff, targetBasename);
|
|
29298
|
-
const patchFile =
|
|
29299
|
-
|
|
29550
|
+
const patchFile = join9(scratchDir, "proposal.patch");
|
|
29551
|
+
writeFileSync4(patchFile, normalizedDiff);
|
|
29300
29552
|
const bin = gitBin ?? "git";
|
|
29301
29553
|
const baseArgs = [
|
|
29302
29554
|
"apply",
|
|
@@ -29326,7 +29578,7 @@ function applyPatch(unifiedDiff, configPath, gitBin) {
|
|
|
29326
29578
|
detail: `git apply failed: ${(real.stderr || "").trim().slice(0, 500)}`
|
|
29327
29579
|
};
|
|
29328
29580
|
}
|
|
29329
|
-
const after =
|
|
29581
|
+
const after = readFileSync7(scratchFile, "utf8");
|
|
29330
29582
|
return { ok: true, after };
|
|
29331
29583
|
} finally {
|
|
29332
29584
|
rmSync2(scratchDir, { recursive: true, force: true });
|
|
@@ -29524,7 +29776,7 @@ function validateConfigEdit(opts) {
|
|
|
29524
29776
|
return schemaErr;
|
|
29525
29777
|
let beforeData = {};
|
|
29526
29778
|
try {
|
|
29527
|
-
const beforeRaw =
|
|
29779
|
+
const beforeRaw = existsSync9(opts.configPath) ? readFileSync7(opts.configPath, "utf8") : "";
|
|
29528
29780
|
const beforeDoc = import_yaml5.parseDocument(beforeRaw, { merge: false, strict: false });
|
|
29529
29781
|
beforeData = beforeDoc.toJS();
|
|
29530
29782
|
} catch {
|
|
@@ -29770,19 +30022,21 @@ function phaseLine(s) {
|
|
|
29770
30022
|
case "apply":
|
|
29771
30023
|
return "applying — regenerating compose";
|
|
29772
30024
|
case "canary-start":
|
|
29773
|
-
return `canary — restarting ${s.agent
|
|
30025
|
+
return `canary — restarting ${s.agent ? `\`${s.agent}\`` : "canary agent"}`;
|
|
29774
30026
|
case "canary-pass":
|
|
29775
|
-
return `canary passed (${s.agent
|
|
30027
|
+
return `canary passed (${s.agent ? `\`${s.agent}\`` : "canary"}) — rolling the rest`;
|
|
29776
30028
|
case "canary-fail":
|
|
29777
|
-
return `canary failed (${s.agent
|
|
30029
|
+
return `canary failed (${s.agent ? `\`${s.agent}\`` : "canary"})`;
|
|
29778
30030
|
case "agent-start":
|
|
29779
|
-
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — restarting ${s.agent
|
|
30031
|
+
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — restarting ${s.agent ? `\`${s.agent}\`` : ""}`.trim();
|
|
29780
30032
|
case "agent-done":
|
|
29781
|
-
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — ${s.agent
|
|
30033
|
+
return `agent ${s.n ?? "?"}/${s.m ?? "?"} — ${s.agent ? `\`${s.agent}\`` : ""} done`.trim();
|
|
29782
30034
|
case "persist-pin":
|
|
29783
30035
|
return "persisting pin";
|
|
30036
|
+
case "web-refresh":
|
|
30037
|
+
return "refreshing web dashboard (webd install)";
|
|
29784
30038
|
case "hostd-web-deferred":
|
|
29785
|
-
return "hostd
|
|
30039
|
+
return "hostd refresh deferred (run host-side)";
|
|
29786
30040
|
case "self-bump":
|
|
29787
30041
|
return "hostd refreshing itself to the target (brief control blip)…";
|
|
29788
30042
|
case "self-bump-done":
|
|
@@ -29791,18 +30045,147 @@ function phaseLine(s) {
|
|
|
29791
30045
|
return "starting";
|
|
29792
30046
|
}
|
|
29793
30047
|
}
|
|
29794
|
-
function
|
|
29795
|
-
const
|
|
29796
|
-
|
|
30048
|
+
function formatDurationMs(ms) {
|
|
30049
|
+
const totalSec = Math.max(0, Math.round(ms / 1000));
|
|
30050
|
+
if (totalSec < 60)
|
|
30051
|
+
return `${totalSec}s`;
|
|
30052
|
+
const totalMin = Math.floor(totalSec / 60);
|
|
30053
|
+
const sec = totalSec % 60;
|
|
30054
|
+
if (totalMin < 60)
|
|
30055
|
+
return sec > 0 ? `${totalMin}m ${sec}s` : `${totalMin}m`;
|
|
30056
|
+
const hr = Math.floor(totalMin / 60);
|
|
30057
|
+
const min = totalMin % 60;
|
|
30058
|
+
return min > 0 ? `${hr}h ${min}m` : `${hr}h`;
|
|
30059
|
+
}
|
|
30060
|
+
function shortRequestId(id) {
|
|
30061
|
+
return id.length > 16 ? id.slice(0, 16) + "…" : id;
|
|
30062
|
+
}
|
|
30063
|
+
function headerLine(s, icon) {
|
|
30064
|
+
const arrow = s.fromVersion ? `\`${s.fromVersion}\` → \`${s.target}\`` : `→ \`${s.target}\``;
|
|
30065
|
+
const req = s.requestId ? ` · req \`${shortRequestId(s.requestId)}\`` : "";
|
|
30066
|
+
return `${icon} **Rollout** ${arrow}${req}`;
|
|
30067
|
+
}
|
|
30068
|
+
function checklistLines(s, compact) {
|
|
30069
|
+
const agents = s.agents ?? [];
|
|
30070
|
+
const lines = [];
|
|
30071
|
+
let doneCount = 0;
|
|
30072
|
+
let pendingNamed = 0;
|
|
30073
|
+
for (const a of agents) {
|
|
30074
|
+
const canary = a.canary ? " (canary)" : "";
|
|
30075
|
+
const dur = a.durationMs !== undefined ? ` — ${formatDurationMs(a.durationMs)}` : "";
|
|
30076
|
+
switch (a.status) {
|
|
30077
|
+
case "done":
|
|
30078
|
+
if (compact)
|
|
30079
|
+
doneCount += 1;
|
|
30080
|
+
else
|
|
30081
|
+
lines.push(`- ✓ \`${a.name}\`${canary}${dur}`);
|
|
30082
|
+
break;
|
|
30083
|
+
case "running":
|
|
30084
|
+
lines.push(`- ⏳ \`${a.name}\`${canary} — restarting…`);
|
|
30085
|
+
break;
|
|
30086
|
+
case "failed":
|
|
30087
|
+
lines.push(`- ✗ \`${a.name}\`${canary} — failed${dur}`);
|
|
30088
|
+
break;
|
|
30089
|
+
default:
|
|
30090
|
+
if (compact)
|
|
30091
|
+
pendingNamed += 1;
|
|
30092
|
+
else
|
|
30093
|
+
lines.push(`- · \`${a.name}\`${canary}`);
|
|
30094
|
+
}
|
|
30095
|
+
}
|
|
30096
|
+
if (doneCount > 0)
|
|
30097
|
+
lines.unshift(`- ✓ ${doneCount} done`);
|
|
30098
|
+
const unseen = s.m !== undefined ? Math.max(0, s.m - agents.length) : 0;
|
|
30099
|
+
const pendingTotal = unseen + pendingNamed;
|
|
30100
|
+
if (pendingTotal > 0 && agents.length > 0) {
|
|
30101
|
+
lines.push(`- · ${pendingTotal} more pending`);
|
|
30102
|
+
}
|
|
30103
|
+
return lines;
|
|
30104
|
+
}
|
|
30105
|
+
function etaLine(s) {
|
|
30106
|
+
if (s.m === undefined)
|
|
30107
|
+
return null;
|
|
30108
|
+
const done = (s.agents ?? []).filter((a) => a.status === "done" && a.durationMs !== undefined);
|
|
30109
|
+
if (done.length === 0)
|
|
30110
|
+
return null;
|
|
30111
|
+
const remaining = s.m - done.length;
|
|
30112
|
+
if (remaining <= 0)
|
|
30113
|
+
return null;
|
|
30114
|
+
const meanMs = done.reduce((t, a) => t + (a.durationMs ?? 0), 0) / done.length;
|
|
30115
|
+
return `~${formatDurationMs(meanMs * remaining)} left (rough est.)`;
|
|
30116
|
+
}
|
|
30117
|
+
function deferredLines(target) {
|
|
30118
|
+
const bare = target.trim().replace(/^v/, "");
|
|
30119
|
+
return [
|
|
30120
|
+
`**Deferred (still on the prior version — run host-side):**`,
|
|
30121
|
+
`- \`switchroom-web\` — \`switchroom webd install --tag ${target}\``,
|
|
30122
|
+
`- host operator CLI — \`sudo npm i -g switchroom@${bare}\``,
|
|
30123
|
+
`- hostd template regen (only if the release changed hostd mounts/env) — \`switchroom hostd install --tag ${target}\``
|
|
30124
|
+
];
|
|
30125
|
+
}
|
|
30126
|
+
var MAX_RENDER_CHARS = 3800;
|
|
30127
|
+
function rolledList(rolled, compact) {
|
|
30128
|
+
if (rolled.length === 0)
|
|
30129
|
+
return "none";
|
|
30130
|
+
if (compact)
|
|
30131
|
+
return `${rolled.length} agent(s)`;
|
|
30132
|
+
return rolled.map((r) => `\`${r}\``).join(", ");
|
|
30133
|
+
}
|
|
30134
|
+
function renderWith(s, compact) {
|
|
30135
|
+
const rolled = s.rolled ?? [];
|
|
30136
|
+
const rolledCount = rolled.length;
|
|
30137
|
+
const checklist = checklistLines(s, compact);
|
|
30138
|
+
const elapsedMs = s.elapsedMs ?? (s.startedAtMs !== undefined && s.nowMs !== undefined ? s.nowMs - s.startedAtMs : undefined);
|
|
29797
30139
|
if (s.terminal === "completed") {
|
|
29798
|
-
|
|
30140
|
+
const parts2 = [headerLine(s, "✅")];
|
|
30141
|
+
let summary = `**Done** — rolled ${rolledCount}${s.m !== undefined ? `/${s.m}` : ""} agent(s)`;
|
|
30142
|
+
if (elapsedMs !== undefined)
|
|
30143
|
+
summary += ` in ${formatDurationMs(elapsedMs)}`;
|
|
30144
|
+
summary += compact ? "." : `: ${rolledList(rolled, compact)}.`;
|
|
30145
|
+
parts2.push(summary);
|
|
30146
|
+
if (checklist.length > 0)
|
|
30147
|
+
parts2.push("", ...checklist);
|
|
30148
|
+
if (s.deferred !== false)
|
|
30149
|
+
parts2.push("", ...deferredLines(s.target));
|
|
30150
|
+
return parts2.join(`
|
|
30151
|
+
`);
|
|
29799
30152
|
}
|
|
29800
30153
|
if (s.terminal === "error") {
|
|
29801
|
-
const where = s.failedStep ? ` at
|
|
29802
|
-
|
|
30154
|
+
const where = s.failedStep ? ` at \`${s.failedStep}\`${s.failedAgent ? ` (\`${s.failedAgent}\` → ${s.got ?? "unreachable"})` : ""}` : "";
|
|
30155
|
+
const parts2 = [headerLine(s, "❌")];
|
|
30156
|
+
let summary = `**STOPPED**${where}`;
|
|
30157
|
+
if (elapsedMs !== undefined)
|
|
30158
|
+
summary += ` after ${formatDurationMs(elapsedMs)}`;
|
|
30159
|
+
summary += `. Rolled before stop: ${rolledList(rolled, compact)}.`;
|
|
30160
|
+
parts2.push(summary);
|
|
30161
|
+
if (checklist.length > 0)
|
|
30162
|
+
parts2.push("", ...checklist);
|
|
30163
|
+
return parts2.join(`
|
|
30164
|
+
`);
|
|
29803
30165
|
}
|
|
29804
|
-
const
|
|
29805
|
-
|
|
30166
|
+
const parts = [headerLine(s, "⏳"), `**Phase:** ${phaseLine(s)}`];
|
|
30167
|
+
if (checklist.length > 0)
|
|
30168
|
+
parts.push("", ...checklist);
|
|
30169
|
+
const footer = [];
|
|
30170
|
+
if (s.m !== undefined)
|
|
30171
|
+
footer.push(`${rolledCount}/${s.m} rolled`);
|
|
30172
|
+
else if (rolledCount > 0)
|
|
30173
|
+
footer.push(`${rolledCount} rolled`);
|
|
30174
|
+
if (elapsedMs !== undefined)
|
|
30175
|
+
footer.push(`elapsed ${formatDurationMs(elapsedMs)}`);
|
|
30176
|
+
const eta = etaLine(s);
|
|
30177
|
+
if (eta)
|
|
30178
|
+
footer.push(eta);
|
|
30179
|
+
if (footer.length > 0)
|
|
30180
|
+
parts.push("", footer.join(" · "));
|
|
30181
|
+
return parts.join(`
|
|
30182
|
+
`);
|
|
30183
|
+
}
|
|
30184
|
+
function renderRolloutStatus(s) {
|
|
30185
|
+
const full = renderWith(s, false);
|
|
30186
|
+
if (full.length <= MAX_RENDER_CHARS)
|
|
30187
|
+
return full;
|
|
30188
|
+
return renderWith(s, true);
|
|
29806
30189
|
}
|
|
29807
30190
|
|
|
29808
30191
|
// src/scheduler/dispatch.ts
|
|
@@ -29887,11 +30270,11 @@ function resolveDigests(imageRefs) {
|
|
|
29887
30270
|
return out;
|
|
29888
30271
|
}
|
|
29889
30272
|
function readCachedInstallType(bindRoot) {
|
|
29890
|
-
const cacheDir =
|
|
29891
|
-
const cachePath =
|
|
29892
|
-
if (
|
|
30273
|
+
const cacheDir = join10(bindRoot, ".switchroom");
|
|
30274
|
+
const cachePath = join10(cacheDir, "install-type.json");
|
|
30275
|
+
if (existsSync10(cachePath)) {
|
|
29893
30276
|
try {
|
|
29894
|
-
const raw =
|
|
30277
|
+
const raw = readFileSync8(cachePath, "utf-8");
|
|
29895
30278
|
const parsed = JSON.parse(raw);
|
|
29896
30279
|
if (parsed && typeof parsed.install_type === "string" && typeof parsed.detected_at === "string") {
|
|
29897
30280
|
return parsed;
|
|
@@ -29908,10 +30291,10 @@ function readCachedInstallType(bindRoot) {
|
|
|
29908
30291
|
source_paths: ctx.source_paths
|
|
29909
30292
|
};
|
|
29910
30293
|
try {
|
|
29911
|
-
|
|
30294
|
+
mkdirSync4(cacheDir, { recursive: true });
|
|
29912
30295
|
const tmp = `${cachePath}.tmp`;
|
|
29913
|
-
|
|
29914
|
-
|
|
30296
|
+
writeFileSync5(tmp, JSON.stringify(payload, null, 2), { mode: 420 });
|
|
30297
|
+
renameSync3(tmp, cachePath);
|
|
29915
30298
|
} catch {}
|
|
29916
30299
|
return payload;
|
|
29917
30300
|
}
|
|
@@ -29940,7 +30323,7 @@ function writeFileInPlacePreservingInode(targetPath, content) {
|
|
|
29940
30323
|
} finally {
|
|
29941
30324
|
closeSync3(fd);
|
|
29942
30325
|
}
|
|
29943
|
-
const readBack =
|
|
30326
|
+
const readBack = readFileSync8(targetPath);
|
|
29944
30327
|
if (readBack.length !== buf.length) {
|
|
29945
30328
|
throw new Error(`in-place write short: wrote ${buf.length} bytes but read back ${readBack.length}`);
|
|
29946
30329
|
}
|
|
@@ -29965,7 +30348,7 @@ class HostdServer {
|
|
|
29965
30348
|
this.opts = opts;
|
|
29966
30349
|
}
|
|
29967
30350
|
async start() {
|
|
29968
|
-
const hostdDir =
|
|
30351
|
+
const hostdDir = join10(this.opts.homeDir, ".switchroom", "hostd");
|
|
29969
30352
|
await mkdir(hostdDir, { recursive: true });
|
|
29970
30353
|
await chmod(hostdDir, 493).catch(() => {
|
|
29971
30354
|
return;
|
|
@@ -29980,13 +30363,13 @@ class HostdServer {
|
|
|
29980
30363
|
}
|
|
29981
30364
|
try {
|
|
29982
30365
|
for (const name of agentNames) {
|
|
29983
|
-
const dir =
|
|
29984
|
-
const sockPath =
|
|
30366
|
+
const dir = join10(hostdDir, name);
|
|
30367
|
+
const sockPath = join10(dir, "sock");
|
|
29985
30368
|
await mkdir(dir, { recursive: true });
|
|
29986
30369
|
await chmod(dir, 493).catch(() => {
|
|
29987
30370
|
return;
|
|
29988
30371
|
});
|
|
29989
|
-
if (
|
|
30372
|
+
if (existsSync10(sockPath))
|
|
29990
30373
|
await unlink(sockPath).catch(() => {
|
|
29991
30374
|
return;
|
|
29992
30375
|
});
|
|
@@ -30017,13 +30400,13 @@ class HostdServer {
|
|
|
30017
30400
|
process.stderr.write(`hostd: SWITCHROOM_HOSTD_OPERATOR_UID='${opUidStr}' is not a positive integer; skipping operator listener
|
|
30018
30401
|
`);
|
|
30019
30402
|
} else {
|
|
30020
|
-
const dir =
|
|
30021
|
-
const sockPath =
|
|
30403
|
+
const dir = join10(hostdDir, "operator");
|
|
30404
|
+
const sockPath = join10(dir, "sock");
|
|
30022
30405
|
await mkdir(dir, { recursive: true });
|
|
30023
30406
|
await chmod(dir, 493).catch(() => {
|
|
30024
30407
|
return;
|
|
30025
30408
|
});
|
|
30026
|
-
if (
|
|
30409
|
+
if (existsSync10(sockPath))
|
|
30027
30410
|
await unlink(sockPath).catch(() => {
|
|
30028
30411
|
return;
|
|
30029
30412
|
});
|
|
@@ -30059,11 +30442,11 @@ class HostdServer {
|
|
|
30059
30442
|
}
|
|
30060
30443
|
async reconcileOrphanedFleetMutations() {
|
|
30061
30444
|
const path2 = this.auditLogPath();
|
|
30062
|
-
if (!
|
|
30445
|
+
if (!existsSync10(path2))
|
|
30063
30446
|
return;
|
|
30064
30447
|
let raw;
|
|
30065
30448
|
try {
|
|
30066
|
-
raw =
|
|
30449
|
+
raw = readFileSync8(path2, "utf-8");
|
|
30067
30450
|
} catch {
|
|
30068
30451
|
return;
|
|
30069
30452
|
}
|
|
@@ -30393,10 +30776,10 @@ class HostdServer {
|
|
|
30393
30776
|
missingApplyAssets() {
|
|
30394
30777
|
const root = this.opts.applyAssetsRoot ?? resolve8(import.meta.dirname, "../..");
|
|
30395
30778
|
return [
|
|
30396
|
-
|
|
30397
|
-
|
|
30398
|
-
|
|
30399
|
-
].filter((p) => !
|
|
30779
|
+
join10(root, "profiles"),
|
|
30780
|
+
join10(root, "profiles", "default"),
|
|
30781
|
+
join10(root, "vendor", "hindsight-memory")
|
|
30782
|
+
].filter((p) => !existsSync10(p));
|
|
30400
30783
|
}
|
|
30401
30784
|
applyAssetPreflight(request_id, started) {
|
|
30402
30785
|
const missing = this.missingApplyAssets();
|
|
@@ -30560,19 +30943,19 @@ class HostdServer {
|
|
|
30560
30943
|
return entry;
|
|
30561
30944
|
}
|
|
30562
30945
|
hostdDirPath() {
|
|
30563
|
-
return
|
|
30946
|
+
return join10(this.opts.homeDir, ".switchroom", "hostd");
|
|
30564
30947
|
}
|
|
30565
30948
|
hostdDirHostPath() {
|
|
30566
30949
|
const hostHome = this.opts.hostHomeDir ?? process.env.SWITCHROOM_HOST_HOME?.trim() ?? this.opts.homeDir;
|
|
30567
|
-
return
|
|
30950
|
+
return join10(hostHome, ".switchroom", "hostd");
|
|
30568
30951
|
}
|
|
30569
30952
|
async beginSelfBump(req, caller, started) {
|
|
30570
30953
|
const ownVersion = this.opts.selfVersion ?? SWITCHROOM_VERSION;
|
|
30571
|
-
const composePath =
|
|
30572
|
-
if (!
|
|
30954
|
+
const composePath = join10(this.hostdDirPath(), "docker-compose.yml");
|
|
30955
|
+
if (!existsSync10(composePath)) {
|
|
30573
30956
|
return deniedResponse(req.request_id, `rollout: hostd's CLI (v${ownVersion}) is older than the target ` + `${req.args.pin} and needs a self-bump first, but its compose file ` + `is missing at ${composePath}. Run \`switchroom hostd install ` + `--tag ${req.args.pin}\` on the host, then re-run the roll. ` + `Nothing was changed.`, Date.now() - started);
|
|
30574
30957
|
}
|
|
30575
|
-
const before =
|
|
30958
|
+
const before = readFileSync8(composePath, "utf8");
|
|
30576
30959
|
const bumped = bumpHostdComposeImageTag(before, req.args.pin);
|
|
30577
30960
|
if (!bumped) {
|
|
30578
30961
|
return deniedResponse(req.request_id, `rollout: hostd needs a self-bump to ${req.args.pin} but no ` + `switchroom-hostd image line was found in ${composePath} ` + `(hand-edited compose?). Run \`switchroom hostd install --tag ` + `${req.args.pin}\` on the host, then re-run the roll. Nothing ` + `was changed.`, Date.now() - started);
|
|
@@ -30593,10 +30976,10 @@ class HostdServer {
|
|
|
30593
30976
|
try {
|
|
30594
30977
|
const baks = readdirSync3(this.hostdDirPath()).filter((f) => f.startsWith("docker-compose.yml.bak-selfbump-")).sort();
|
|
30595
30978
|
for (const f of baks.slice(0, Math.max(0, baks.length - 4))) {
|
|
30596
|
-
|
|
30979
|
+
unlinkSync2(join10(this.hostdDirPath(), f));
|
|
30597
30980
|
}
|
|
30598
30981
|
} catch {}
|
|
30599
|
-
const markerPath =
|
|
30982
|
+
const markerPath = join10(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
|
|
30600
30983
|
const marker = {
|
|
30601
30984
|
v: 1,
|
|
30602
30985
|
request_id: req.request_id,
|
|
@@ -30609,8 +30992,8 @@ class HostdServer {
|
|
|
30609
30992
|
prior_hostd_version: ownVersion
|
|
30610
30993
|
};
|
|
30611
30994
|
copyFileSync2(composePath, bakPath);
|
|
30612
|
-
|
|
30613
|
-
|
|
30995
|
+
writeFileSync5(composePath, bumped.yaml, "utf8");
|
|
30996
|
+
writeFileSync5(markerPath, encodePendingRolloutMarker(marker), {
|
|
30614
30997
|
mode: 384
|
|
30615
30998
|
});
|
|
30616
30999
|
const entry = {
|
|
@@ -30635,7 +31018,7 @@ class HostdServer {
|
|
|
30635
31018
|
const rollbackBump = () => {
|
|
30636
31019
|
try {
|
|
30637
31020
|
copyFileSync2(bakPath, composePath);
|
|
30638
|
-
|
|
31021
|
+
unlinkSync2(markerPath);
|
|
30639
31022
|
} catch (e) {
|
|
30640
31023
|
process.stderr.write(`hostd: self-bump rollback failed (compose/marker may need manual ` + `restore from ${bakPath}): ${e.message}
|
|
30641
31024
|
`);
|
|
@@ -30678,7 +31061,7 @@ class HostdServer {
|
|
|
30678
31061
|
rollbackBump();
|
|
30679
31062
|
entry.result = "error";
|
|
30680
31063
|
entry.finished_at = Date.now();
|
|
30681
|
-
entry.error = `self-bump helper failed (container exit ${Number.isFinite(code) ? code : "unknown"}). ` + `See ${
|
|
31064
|
+
entry.error = `self-bump helper failed (container exit ${Number.isFinite(code) ? code : "unknown"}). ` + `See ${join10(this.hostdDirHostPath(), SELF_BUMP_LOG_FILENAME)} on the host. ` + `Compose bump rolled back. Fallback: \`switchroom hostd install ` + `--tag ${req.args.pin}\` host-side, then re-run the roll.`;
|
|
30682
31065
|
if (this.fleetMutationInFlight && this.fleetMutationInFlight.request_id === entry.request_id) {
|
|
30683
31066
|
this.fleetMutationInFlight = null;
|
|
30684
31067
|
}
|
|
@@ -30702,15 +31085,15 @@ class HostdServer {
|
|
|
30702
31085
|
};
|
|
30703
31086
|
}
|
|
30704
31087
|
async resumePendingSelfBumpRollout() {
|
|
30705
|
-
const markerPath =
|
|
30706
|
-
if (!
|
|
31088
|
+
const markerPath = join10(this.hostdDirPath(), SELF_BUMP_MARKER_FILENAME);
|
|
31089
|
+
if (!existsSync10(markerPath))
|
|
30707
31090
|
return;
|
|
30708
31091
|
let raw;
|
|
30709
31092
|
try {
|
|
30710
|
-
raw =
|
|
31093
|
+
raw = readFileSync8(markerPath, "utf8");
|
|
30711
31094
|
} finally {
|
|
30712
31095
|
try {
|
|
30713
|
-
|
|
31096
|
+
unlinkSync2(markerPath);
|
|
30714
31097
|
} catch {}
|
|
30715
31098
|
}
|
|
30716
31099
|
const marker = parsePendingRolloutMarker(raw);
|
|
@@ -30747,7 +31130,7 @@ class HostdServer {
|
|
|
30747
31130
|
return;
|
|
30748
31131
|
}
|
|
30749
31132
|
if (needsSelfBump(ownVersion, marker.pin)) {
|
|
30750
|
-
await failResume(`hostd is still on v${ownVersion} after the self-bump to ` + `${marker.pin} (helper failed? see ` + `${
|
|
31133
|
+
await failResume(`hostd is still on v${ownVersion} after the self-bump to ` + `${marker.pin} (helper failed? see ` + `${join10(this.hostdDirHostPath(), SELF_BUMP_LOG_FILENAME)}). ` + `Fallback: \`switchroom hostd install --tag ${marker.pin}\` ` + `host-side, then re-issue the rollout.`);
|
|
30751
31134
|
return;
|
|
30752
31135
|
}
|
|
30753
31136
|
process.stderr.write(`hostd: resuming rollout ${marker.request_id} → ${marker.pin} after ` + `self-bump (v${marker.prior_hostd_version} → v${ownVersion})
|
|
@@ -30841,7 +31224,7 @@ class HostdServer {
|
|
|
30841
31224
|
if (caller.kind === "agent" && this.opts.config.agents[caller.name]?.admin !== true) {
|
|
30842
31225
|
let beforeContent;
|
|
30843
31226
|
try {
|
|
30844
|
-
beforeContent =
|
|
31227
|
+
beforeContent = readFileSync8(configPath, "utf-8");
|
|
30845
31228
|
} catch {
|
|
30846
31229
|
beforeContent = "";
|
|
30847
31230
|
}
|
|
@@ -30945,7 +31328,7 @@ class HostdServer {
|
|
|
30945
31328
|
try {
|
|
30946
31329
|
let snapshot;
|
|
30947
31330
|
try {
|
|
30948
|
-
snapshot =
|
|
31331
|
+
snapshot = readFileSync8(configPath, "utf-8");
|
|
30949
31332
|
} catch (e) {
|
|
30950
31333
|
await approval.finalize({
|
|
30951
31334
|
outcome: "reconcile_failed_rolled_back",
|
|
@@ -31197,8 +31580,8 @@ ${output.recovery.stderr}` : "";
|
|
|
31197
31580
|
if (this.opts.imageRefsForDigests)
|
|
31198
31581
|
return this.opts.imageRefsForDigests();
|
|
31199
31582
|
try {
|
|
31200
|
-
const composePath =
|
|
31201
|
-
if (!
|
|
31583
|
+
const composePath = join10(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
|
|
31584
|
+
if (!existsSync10(composePath))
|
|
31202
31585
|
return [];
|
|
31203
31586
|
const r = spawnSync3("docker", [
|
|
31204
31587
|
"compose",
|
|
@@ -31306,6 +31689,9 @@ ${output.recovery.stderr}` : "";
|
|
|
31306
31689
|
target: entry.pin ?? "",
|
|
31307
31690
|
rolled: entry.rolled,
|
|
31308
31691
|
terminal: entry.result === "completed" ? "completed" : "error",
|
|
31692
|
+
requestId: entry.request_id,
|
|
31693
|
+
...entry.prior_pin ? { fromVersion: entry.prior_pin } : {},
|
|
31694
|
+
...entry.finished_at !== null ? { elapsedMs: entry.finished_at - entry.started_at } : {},
|
|
31309
31695
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
31310
31696
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
31311
31697
|
...entry.got !== undefined ? { got: entry.got } : {}
|
|
@@ -31334,10 +31720,10 @@ ${output.recovery.stderr}` : "";
|
|
|
31334
31720
|
}
|
|
31335
31721
|
rolloutRowExistsInLog(targetRequestId) {
|
|
31336
31722
|
const path2 = this.auditLogPath();
|
|
31337
|
-
if (!
|
|
31723
|
+
if (!existsSync10(path2))
|
|
31338
31724
|
return false;
|
|
31339
31725
|
try {
|
|
31340
|
-
const raw =
|
|
31726
|
+
const raw = readFileSync8(path2, "utf-8");
|
|
31341
31727
|
return latestRolloutRowForRequest(raw, targetRequestId) !== null;
|
|
31342
31728
|
} catch {
|
|
31343
31729
|
return false;
|
|
@@ -31345,11 +31731,11 @@ ${output.recovery.stderr}` : "";
|
|
|
31345
31731
|
}
|
|
31346
31732
|
rolloutStatusFromLog(responseRequestId, targetRequestId, started) {
|
|
31347
31733
|
const path2 = this.auditLogPath();
|
|
31348
|
-
if (!
|
|
31734
|
+
if (!existsSync10(path2))
|
|
31349
31735
|
return null;
|
|
31350
31736
|
let raw;
|
|
31351
31737
|
try {
|
|
31352
|
-
raw =
|
|
31738
|
+
raw = readFileSync8(path2, "utf-8");
|
|
31353
31739
|
} catch {
|
|
31354
31740
|
return null;
|
|
31355
31741
|
}
|
|
@@ -31428,7 +31814,7 @@ ${output.recovery.stderr}` : "";
|
|
|
31428
31814
|
}
|
|
31429
31815
|
}
|
|
31430
31816
|
auditLogPath() {
|
|
31431
|
-
return this.opts.auditLogPath ??
|
|
31817
|
+
return this.opts.auditLogPath ?? join10(this.opts.homeDir, ".switchroom", "host-control-audit.log");
|
|
31432
31818
|
}
|
|
31433
31819
|
appendAuditRow(row) {
|
|
31434
31820
|
const path2 = this.auditLogPath();
|
|
@@ -31681,7 +32067,7 @@ function isSafeExecArgvElement(s) {
|
|
|
31681
32067
|
}
|
|
31682
32068
|
|
|
31683
32069
|
// src/host-control/approval-gateway.ts
|
|
31684
|
-
import { connect } from "node:net";
|
|
32070
|
+
import { connect as connect2 } from "node:net";
|
|
31685
32071
|
|
|
31686
32072
|
class SocketApprovalGateway {
|
|
31687
32073
|
opts;
|
|
@@ -31699,7 +32085,7 @@ class SocketApprovalGateway {
|
|
|
31699
32085
|
};
|
|
31700
32086
|
}
|
|
31701
32087
|
return await new Promise((resolve9) => {
|
|
31702
|
-
const client2 =
|
|
32088
|
+
const client2 = connect2({ path: sockPath });
|
|
31703
32089
|
let buffer = "";
|
|
31704
32090
|
let resolved = false;
|
|
31705
32091
|
const log = this.opts.log ?? (() => {});
|
|
@@ -31803,7 +32189,7 @@ class SocketApprovalGateway {
|
|
|
31803
32189
|
}
|
|
31804
32190
|
|
|
31805
32191
|
// src/host-control/rollout-relay.ts
|
|
31806
|
-
import { connect as
|
|
32192
|
+
import { connect as connect3 } from "node:net";
|
|
31807
32193
|
|
|
31808
32194
|
class SocketRolloutRelay {
|
|
31809
32195
|
opts;
|
|
@@ -31824,7 +32210,7 @@ class SocketRolloutRelay {
|
|
|
31824
32210
|
return;
|
|
31825
32211
|
}
|
|
31826
32212
|
try {
|
|
31827
|
-
const client2 =
|
|
32213
|
+
const client2 = connect3({ path: sockPath });
|
|
31828
32214
|
client2.setTimeout(5000);
|
|
31829
32215
|
const done = () => {
|
|
31830
32216
|
try {
|
|
@@ -31862,7 +32248,7 @@ class SocketRolloutRelay {
|
|
|
31862
32248
|
}
|
|
31863
32249
|
|
|
31864
32250
|
// src/host-control/rollout-narration-relay.ts
|
|
31865
|
-
import { connect as
|
|
32251
|
+
import { connect as connect4 } from "node:net";
|
|
31866
32252
|
var DEFAULT_POST_REPLY_TIMEOUT_MS = 5000;
|
|
31867
32253
|
|
|
31868
32254
|
class SocketRolloutNarrationRelay {
|
|
@@ -31896,7 +32282,7 @@ class SocketRolloutNarrationRelay {
|
|
|
31896
32282
|
};
|
|
31897
32283
|
let client2;
|
|
31898
32284
|
try {
|
|
31899
|
-
client2 =
|
|
32285
|
+
client2 = connect4({ path: sockPath });
|
|
31900
32286
|
} catch (e) {
|
|
31901
32287
|
log(`narration post connect threw: ${e.message}`);
|
|
31902
32288
|
resolve9(null);
|
|
@@ -31966,7 +32352,7 @@ class SocketRolloutNarrationRelay {
|
|
|
31966
32352
|
if (sockPath === null)
|
|
31967
32353
|
return;
|
|
31968
32354
|
try {
|
|
31969
|
-
const client2 =
|
|
32355
|
+
const client2 = connect4({ path: sockPath });
|
|
31970
32356
|
client2.setTimeout(5000);
|
|
31971
32357
|
const done = () => {
|
|
31972
32358
|
try {
|
|
@@ -32013,6 +32399,47 @@ class LogTailRolloutNarrator {
|
|
|
32013
32399
|
this.relay = relay;
|
|
32014
32400
|
this.opts = opts;
|
|
32015
32401
|
}
|
|
32402
|
+
now() {
|
|
32403
|
+
return this.opts.now?.() ?? Date.now();
|
|
32404
|
+
}
|
|
32405
|
+
static upsertAgent(st, name, patch) {
|
|
32406
|
+
const existing = st.agents.find((a) => a.name === name);
|
|
32407
|
+
if (existing)
|
|
32408
|
+
Object.assign(existing, patch);
|
|
32409
|
+
else
|
|
32410
|
+
st.agents.push({ name, status: "pending", ...patch });
|
|
32411
|
+
}
|
|
32412
|
+
trackAgentProgress(st, phase) {
|
|
32413
|
+
const now = this.now();
|
|
32414
|
+
switch (phase.phase) {
|
|
32415
|
+
case "canary-start":
|
|
32416
|
+
case "agent-start":
|
|
32417
|
+
if (phase.agent) {
|
|
32418
|
+
LogTailRolloutNarrator.upsertAgent(st, phase.agent, {
|
|
32419
|
+
status: "running",
|
|
32420
|
+
startedAtMs: now,
|
|
32421
|
+
...phase.phase === "canary-start" ? { canary: true } : {}
|
|
32422
|
+
});
|
|
32423
|
+
}
|
|
32424
|
+
break;
|
|
32425
|
+
case "canary-pass":
|
|
32426
|
+
case "agent-done":
|
|
32427
|
+
case "canary-fail": {
|
|
32428
|
+
if (!phase.agent)
|
|
32429
|
+
break;
|
|
32430
|
+
const a = st.agents.find((x) => x.name === phase.agent);
|
|
32431
|
+
const durationMs = a?.startedAtMs !== undefined ? now - a.startedAtMs : undefined;
|
|
32432
|
+
LogTailRolloutNarrator.upsertAgent(st, phase.agent, {
|
|
32433
|
+
status: phase.phase === "canary-fail" ? "failed" : "done",
|
|
32434
|
+
...durationMs !== undefined ? { durationMs } : {},
|
|
32435
|
+
...phase.phase !== "agent-done" ? { canary: true } : {}
|
|
32436
|
+
});
|
|
32437
|
+
break;
|
|
32438
|
+
}
|
|
32439
|
+
default:
|
|
32440
|
+
break;
|
|
32441
|
+
}
|
|
32442
|
+
}
|
|
32016
32443
|
static seqFor(phase) {
|
|
32017
32444
|
switch (phase.phase) {
|
|
32018
32445
|
case "self-bump":
|
|
@@ -32031,6 +32458,8 @@ class LogTailRolloutNarrator {
|
|
|
32031
32458
|
return 2 * (phase.n ?? 1) + 0;
|
|
32032
32459
|
case "agent-done":
|
|
32033
32460
|
return 2 * (phase.n ?? 1) + 1;
|
|
32461
|
+
case "web-refresh":
|
|
32462
|
+
return Number.MAX_SAFE_INTEGER - 2;
|
|
32034
32463
|
case "hostd-web-deferred":
|
|
32035
32464
|
return Number.MAX_SAFE_INTEGER - 1;
|
|
32036
32465
|
default:
|
|
@@ -32046,6 +32475,7 @@ class LogTailRolloutNarrator {
|
|
|
32046
32475
|
if (seq < st.lastAppliedSeq)
|
|
32047
32476
|
return;
|
|
32048
32477
|
st.lastAppliedSeq = seq;
|
|
32478
|
+
this.trackAgentProgress(st, phase);
|
|
32049
32479
|
st.render = {
|
|
32050
32480
|
...st.render,
|
|
32051
32481
|
target: phase.target,
|
|
@@ -32053,7 +32483,11 @@ class LogTailRolloutNarrator {
|
|
|
32053
32483
|
...phase.n !== undefined ? { n: phase.n } : {},
|
|
32054
32484
|
...phase.m !== undefined ? { m: phase.m } : {},
|
|
32055
32485
|
...phase.agent !== undefined ? { agent: phase.agent } : {},
|
|
32056
|
-
rolled: entry.rolled
|
|
32486
|
+
rolled: entry.rolled,
|
|
32487
|
+
agents: st.agents,
|
|
32488
|
+
requestId: entry.request_id,
|
|
32489
|
+
...entry.prior_pin ? { fromVersion: entry.prior_pin } : {},
|
|
32490
|
+
startedAtMs: entry.started_at
|
|
32057
32491
|
};
|
|
32058
32492
|
this.scheduleRenderOrPost(entry.request_id);
|
|
32059
32493
|
} catch (e) {
|
|
@@ -32063,13 +32497,29 @@ class LogTailRolloutNarrator {
|
|
|
32063
32497
|
onTerminal(entry) {
|
|
32064
32498
|
try {
|
|
32065
32499
|
const st = this.ensureState(entry);
|
|
32500
|
+
const rolled = new Set(entry.rolled ?? []);
|
|
32501
|
+
for (const a of st.agents) {
|
|
32502
|
+
if (entry.failed_agent && a.name === entry.failed_agent) {
|
|
32503
|
+
a.status = "failed";
|
|
32504
|
+
} else if (rolled.has(a.name)) {
|
|
32505
|
+
a.status = "done";
|
|
32506
|
+
} else if (a.status === "running") {
|
|
32507
|
+
a.status = "pending";
|
|
32508
|
+
}
|
|
32509
|
+
}
|
|
32066
32510
|
st.render = {
|
|
32067
32511
|
target: entry.pin ?? st.render.target,
|
|
32068
32512
|
rolled: entry.rolled ?? st.render.rolled,
|
|
32069
32513
|
terminal: entry.result === "completed" ? "completed" : "error",
|
|
32070
32514
|
...entry.failed_step ? { failedStep: entry.failed_step } : {},
|
|
32071
32515
|
...entry.failed_agent ? { failedAgent: entry.failed_agent } : {},
|
|
32072
|
-
...entry.got !== undefined ? { got: entry.got } : {}
|
|
32516
|
+
...entry.got !== undefined ? { got: entry.got } : {},
|
|
32517
|
+
...st.agents.length > 0 ? { agents: st.agents } : {},
|
|
32518
|
+
...st.render.m !== undefined ? { m: st.render.m } : {},
|
|
32519
|
+
requestId: entry.request_id,
|
|
32520
|
+
...entry.prior_pin ?? st.render.fromVersion ? { fromVersion: entry.prior_pin ?? st.render.fromVersion } : {},
|
|
32521
|
+
elapsedMs: (entry.finished_at ?? this.now()) - entry.started_at,
|
|
32522
|
+
deferred: false
|
|
32073
32523
|
};
|
|
32074
32524
|
st.frozen = true;
|
|
32075
32525
|
if (st.timer) {
|
|
@@ -32094,6 +32544,7 @@ class LogTailRolloutNarrator {
|
|
|
32094
32544
|
postAttempts: 0,
|
|
32095
32545
|
postFailed: false,
|
|
32096
32546
|
render: { target: entry.pin ?? "" },
|
|
32547
|
+
agents: [],
|
|
32097
32548
|
frozen: false,
|
|
32098
32549
|
timer: null,
|
|
32099
32550
|
pendingEditAfterPost: false
|
|
@@ -32132,7 +32583,7 @@ class LogTailRolloutNarrator {
|
|
|
32132
32583
|
return false;
|
|
32133
32584
|
st.posting = true;
|
|
32134
32585
|
st.postAttempts += 1;
|
|
32135
|
-
const text = renderRolloutStatus(st.render);
|
|
32586
|
+
const text = renderRolloutStatus({ ...st.render, nowMs: this.now() });
|
|
32136
32587
|
this.relay.post({ requestId, agentName: st.agentName, text }).then((mid) => {
|
|
32137
32588
|
st.posting = false;
|
|
32138
32589
|
if (mid === null) {
|
|
@@ -32184,7 +32635,7 @@ class LogTailRolloutNarrator {
|
|
|
32184
32635
|
this.tryPost(requestId, true);
|
|
32185
32636
|
return;
|
|
32186
32637
|
}
|
|
32187
|
-
const text = renderRolloutStatus(st.render);
|
|
32638
|
+
const text = renderRolloutStatus({ ...st.render, nowMs: this.now() });
|
|
32188
32639
|
this.relay.edit({
|
|
32189
32640
|
requestId,
|
|
32190
32641
|
agentName: st.agentName,
|
|
@@ -32442,8 +32893,60 @@ function makeRestart(switchroomBin) {
|
|
|
32442
32893
|
|
|
32443
32894
|
// src/host-control/main.ts
|
|
32444
32895
|
import { appendFile as appendFile2 } from "node:fs/promises";
|
|
32896
|
+
async function loadConfigResilient() {
|
|
32897
|
+
try {
|
|
32898
|
+
const config = loadConfig();
|
|
32899
|
+
clearStaleDegradedMarker(homedir6(), (m) => process.stderr.write(`hostd: ${m}
|
|
32900
|
+
`));
|
|
32901
|
+
return config;
|
|
32902
|
+
} catch (err2) {
|
|
32903
|
+
if (!(err2 instanceof ConfigError))
|
|
32904
|
+
throw err2;
|
|
32905
|
+
let configPath;
|
|
32906
|
+
try {
|
|
32907
|
+
configPath = findConfigFile();
|
|
32908
|
+
} catch {
|
|
32909
|
+
configPath = undefined;
|
|
32910
|
+
}
|
|
32911
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join11(homedir6(), ".switchroom", "agents");
|
|
32912
|
+
const listGatewayCandidates = () => {
|
|
32913
|
+
const out = [];
|
|
32914
|
+
try {
|
|
32915
|
+
for (const name of readdirSync4(agentsDir).sort()) {
|
|
32916
|
+
const sock = resolve9(agentsDir, name, "telegram", "gateway.sock");
|
|
32917
|
+
if (existsSync11(sock))
|
|
32918
|
+
out.push({ agent: name, sock });
|
|
32919
|
+
}
|
|
32920
|
+
} catch {}
|
|
32921
|
+
return out;
|
|
32922
|
+
};
|
|
32923
|
+
const log = (m) => {
|
|
32924
|
+
process.stderr.write(`hostd: ${m}
|
|
32925
|
+
`);
|
|
32926
|
+
};
|
|
32927
|
+
const notify = (text) => {
|
|
32928
|
+
const candidates = listGatewayCandidates();
|
|
32929
|
+
if (candidates.length === 0) {
|
|
32930
|
+
log("config-degraded: no gateway socket paths found; relying on marker file");
|
|
32931
|
+
return;
|
|
32932
|
+
}
|
|
32933
|
+
postOperatorNoticeViaGateways(candidates, text, log).then((agent) => {
|
|
32934
|
+
if (agent !== null)
|
|
32935
|
+
log(`config-degraded: operator DM relayed via ${agent}`);
|
|
32936
|
+
});
|
|
32937
|
+
};
|
|
32938
|
+
return waitForConfigRecovery({
|
|
32939
|
+
initialError: err2,
|
|
32940
|
+
homeDir: homedir6(),
|
|
32941
|
+
loadFn: () => loadConfig(),
|
|
32942
|
+
configPath,
|
|
32943
|
+
notify,
|
|
32944
|
+
log
|
|
32945
|
+
});
|
|
32946
|
+
}
|
|
32947
|
+
}
|
|
32445
32948
|
async function main() {
|
|
32446
|
-
const config =
|
|
32949
|
+
const config = await loadConfigResilient();
|
|
32447
32950
|
if (config.host_control?.enabled !== true) {
|
|
32448
32951
|
process.stderr.write(`hostd: refusing to start — host_control.enabled is not true in switchroom.yaml
|
|
32449
32952
|
`);
|
|
@@ -32458,10 +32961,10 @@ async function main() {
|
|
|
32458
32961
|
`);
|
|
32459
32962
|
process.exit(2);
|
|
32460
32963
|
}
|
|
32461
|
-
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ??
|
|
32964
|
+
const agentsDir = process.env.SWITCHROOM_AGENTS_DIR ?? join11(homedir6(), ".switchroom", "agents");
|
|
32462
32965
|
const resolveGatewaySocket = (agentName) => {
|
|
32463
32966
|
const sock = resolve9(agentsDir, agentName, "telegram", "gateway.sock");
|
|
32464
|
-
return
|
|
32967
|
+
return existsSync11(sock) ? sock : null;
|
|
32465
32968
|
};
|
|
32466
32969
|
const approvalGateway = new SocketApprovalGateway({
|
|
32467
32970
|
resolveGatewaySocket,
|
|
@@ -32504,7 +33007,7 @@ async function main() {
|
|
|
32504
33007
|
let releaseWatcher = null;
|
|
32505
33008
|
const autoRel = config.host_control?.auto_release_check;
|
|
32506
33009
|
if (autoRel?.enabled === true) {
|
|
32507
|
-
const eventsLog =
|
|
33010
|
+
const eventsLog = join11(homedir6(), ".switchroom", "release-watcher-events.jsonl");
|
|
32508
33011
|
releaseWatcher = new ReleaseWatcher({
|
|
32509
33012
|
intervalMs: autoRel.interval_minutes * 60000,
|
|
32510
33013
|
checkFn: makeReleaseCheck({
|