switchroom 0.15.25 → 0.15.27

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.
@@ -15013,6 +15013,13 @@ function validateAllCronTopicAliases(config, filePath) {
15013
15013
  throw new ConfigError(`Cron \`topic:\` alias references unknown topic_aliases in ${filePath}`, issues);
15014
15014
  }
15015
15015
  }
15016
+ function resolveAgentsDir(config) {
15017
+ const override = process.env.SWITCHROOM_AGENTS_DIR;
15018
+ if (override && override.length > 0 && override.startsWith("/")) {
15019
+ return override;
15020
+ }
15021
+ return resolveDualPath(config.switchroom.agents_dir);
15022
+ }
15016
15023
 
15017
15024
  // src/agents/compose.ts
15018
15025
  import { createHash } from "node:crypto";
@@ -15045,7 +15052,7 @@ var BIND_MOUNT_EXACT_SOURCE_DENY = new Set(["/var/run/docker.sock"]);
15045
15052
 
15046
15053
  // src/host-control/server.ts
15047
15054
  import { createServer } from "node:net";
15048
- import { spawn, spawnSync as spawnSync2 } from "node:child_process";
15055
+ import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
15049
15056
  import { mkdir, chmod, chown, unlink, appendFile } from "node:fs/promises";
15050
15057
  import {
15051
15058
  readdirSync as readdirSync2,
@@ -15148,6 +15155,20 @@ var DoctorRequestSchema = exports_external.object({
15148
15155
  op: exports_external.literal("doctor"),
15149
15156
  args: exports_external.object({}).optional()
15150
15157
  });
15158
+ var AgentStatusRequestSchema = exports_external.object({
15159
+ ...RequestEnvelope,
15160
+ op: exports_external.literal("agent_status"),
15161
+ args: exports_external.object({
15162
+ name: AgentNameSchema.optional()
15163
+ })
15164
+ });
15165
+ var AgentScheduleRequestSchema = exports_external.object({
15166
+ ...RequestEnvelope,
15167
+ op: exports_external.literal("agent_schedule"),
15168
+ args: exports_external.object({
15169
+ name: AgentNameSchema.optional()
15170
+ })
15171
+ });
15151
15172
  var AgentSmokeRequestSchema = exports_external.object({
15152
15173
  ...RequestEnvelope,
15153
15174
  op: exports_external.literal("agent_smoke"),
@@ -15178,6 +15199,8 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
15178
15199
  AgentExecRequestSchema,
15179
15200
  DoctorRequestSchema,
15180
15201
  AgentSmokeRequestSchema,
15202
+ AgentStatusRequestSchema,
15203
+ AgentScheduleRequestSchema,
15181
15204
  ConfigProposeEditRequestSchema
15182
15205
  ]);
15183
15206
  var ResultSchema = exports_external.enum(["started", "completed", "denied", "error"]);
@@ -15240,6 +15263,7 @@ var ResponseEnvelope = {
15240
15263
  audit_id: exports_external.string().min(1).optional(),
15241
15264
  stdout_tail: exports_external.string().optional(),
15242
15265
  stderr_tail: exports_external.string().optional(),
15266
+ payload: exports_external.string().optional(),
15243
15267
  error: exports_external.string().optional(),
15244
15268
  error_envelope: ErrorEnvelopeSchema.optional()
15245
15269
  };
@@ -20896,12 +20920,148 @@ function classifyBlastRadius(beforeYaml, afterYaml) {
20896
20920
  };
20897
20921
  }
20898
20922
 
20923
+ // src/agents/lifecycle.ts
20924
+ import { execFileSync, spawn, spawnSync as spawnSync2 } from "node:child_process";
20925
+
20926
+ // src/agents/tmux.ts
20927
+ var MAX_BYTES = 10 * 1024 * 1024;
20928
+
20929
+ // src/agents/lifecycle.ts
20930
+ function containerName(name) {
20931
+ return `switchroom-${name}`;
20932
+ }
20933
+ function getAllAgentStatuses(config) {
20934
+ const agentNames = Object.keys(config.agents);
20935
+ const statuses = {};
20936
+ if (agentNames.length === 0)
20937
+ return statuses;
20938
+ const cnByAgent = new Map;
20939
+ const agentByCn = new Map;
20940
+ for (const a of agentNames) {
20941
+ const cn = containerName(a);
20942
+ cnByAgent.set(a, cn);
20943
+ agentByCn.set(cn, a);
20944
+ statuses[a] = { active: "inactive", uptime: null, memory: null, pid: null };
20945
+ }
20946
+ const insp = spawnSync2("docker", [
20947
+ "inspect",
20948
+ "--format",
20949
+ "{{.Name}}|{{.State.Status}}|{{.State.StartedAt}}|{{.State.Pid}}",
20950
+ ...cnByAgent.values()
20951
+ ], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15000 });
20952
+ for (const line of (insp.stdout ?? "").split(`
20953
+ `)) {
20954
+ const t = line.trim();
20955
+ if (!t)
20956
+ continue;
20957
+ const [rawName, status, startedAt, pidStr] = t.split("|");
20958
+ const cn = (rawName ?? "").replace(/^\//, "");
20959
+ const agent = agentByCn.get(cn);
20960
+ if (!agent)
20961
+ continue;
20962
+ const pidNum = parseInt(pidStr ?? "", 10);
20963
+ statuses[agent] = {
20964
+ active: status === "running" ? "active" : status || "inactive",
20965
+ uptime: startedAt && startedAt !== "0001-01-01T00:00:00Z" ? startedAt : null,
20966
+ memory: null,
20967
+ pid: Number.isFinite(pidNum) && pidNum > 0 ? pidNum : null
20968
+ };
20969
+ }
20970
+ const stats = spawnSync2("docker", ["stats", "--no-stream", "--format", "{{.Name}}|{{.MemUsage}}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"], timeout: 15000 });
20971
+ if (stats.status === 0) {
20972
+ for (const line of (stats.stdout ?? "").split(`
20973
+ `)) {
20974
+ const t = line.trim();
20975
+ if (!t)
20976
+ continue;
20977
+ const [cn, memUsage] = t.split("|");
20978
+ const agent = cn ? agentByCn.get(cn) : undefined;
20979
+ if (!agent || statuses[agent].active !== "active")
20980
+ continue;
20981
+ const first = (memUsage ?? "").split("/")[0]?.trim();
20982
+ if (!first)
20983
+ continue;
20984
+ const m = first.match(/([\d.]+)\s*([KMG]i?B)/i);
20985
+ if (m) {
20986
+ const val = parseFloat(m[1]);
20987
+ const unit = m[2].toUpperCase();
20988
+ let mb = val;
20989
+ if (unit.startsWith("K"))
20990
+ mb = val / 1024;
20991
+ else if (unit.startsWith("G"))
20992
+ mb = val * 1024;
20993
+ statuses[agent].memory = `${Math.round(mb)}MB`;
20994
+ } else {
20995
+ statuses[agent].memory = first;
20996
+ }
20997
+ }
20998
+ }
20999
+ return statuses;
21000
+ }
21001
+
21002
+ // src/scheduler/dispatch.ts
21003
+ import { createHash as createHash4 } from "node:crypto";
21004
+ function collectScheduleEntries(config) {
21005
+ const out = [];
21006
+ const agentNames = Object.keys(config.agents).sort();
21007
+ for (const agent of agentNames) {
21008
+ const raw = config.agents[agent];
21009
+ if (!raw)
21010
+ continue;
21011
+ const resolved = resolveAgentConfig(config.defaults, config.profiles, raw);
21012
+ const schedule = resolved.schedule ?? [];
21013
+ for (let i = 0;i < schedule.length; i++) {
21014
+ const entry = schedule[i];
21015
+ const auditMaterial = entry.prompt ?? `action:${JSON.stringify(entry.action ?? {})}`;
21016
+ out.push({
21017
+ agent,
21018
+ scheduleIndex: i,
21019
+ cron: entry.cron,
21020
+ ...entry.prompt !== undefined ? { prompt: entry.prompt } : {},
21021
+ promptKey: createHash4("sha256").update(auditMaterial).digest("hex").slice(0, 12),
21022
+ ...entry.topic !== undefined ? { topic: entry.topic } : {},
21023
+ ...entry.kind !== undefined ? { kind: entry.kind } : {},
21024
+ ...entry.model !== undefined ? { model: entry.model } : {},
21025
+ ...entry.context !== undefined ? { context: entry.context } : {},
21026
+ ...entry.poll !== undefined ? { poll: entry.poll } : {},
21027
+ ...entry.action !== undefined ? { action: entry.action } : {}
21028
+ });
21029
+ }
21030
+ }
21031
+ return out;
21032
+ }
21033
+
21034
+ // src/agent-scheduler/replay.ts
21035
+ var STALE_LOOKBACK_MAX_MIN = 14 * 24 * 60;
21036
+ function readRecentFires(jsonlPath) {
21037
+ const fs2 = __require("node:fs");
21038
+ if (!fs2.existsSync(jsonlPath))
21039
+ return [];
21040
+ let raw;
21041
+ try {
21042
+ raw = fs2.readFileSync(jsonlPath, "utf8");
21043
+ } catch {
21044
+ return [];
21045
+ }
21046
+ const out = [];
21047
+ for (const line of raw.split(`
21048
+ `)) {
21049
+ const trimmed = line.trim();
21050
+ if (trimmed.length === 0)
21051
+ continue;
21052
+ try {
21053
+ out.push(JSON.parse(trimmed));
21054
+ } catch {}
21055
+ }
21056
+ return out;
21057
+ }
21058
+
20899
21059
  // src/host-control/server.ts
20900
21060
  function resolveDigests(imageRefs) {
20901
21061
  const out = new Map;
20902
21062
  for (const ref of imageRefs) {
20903
21063
  try {
20904
- const r = spawnSync2("docker", ["inspect", "--format={{index .RepoDigests 0}}", ref], { encoding: "utf-8", timeout: 5000 });
21064
+ const r = spawnSync3("docker", ["inspect", "--format={{index .RepoDigests 0}}", ref], { encoding: "utf-8", timeout: 5000 });
20905
21065
  if (r.status !== 0)
20906
21066
  continue;
20907
21067
  const trimmed = (r.stdout ?? "").trim();
@@ -21204,6 +21364,12 @@ class HostdServer {
21204
21364
  case "agent_smoke":
21205
21365
  resp = await this.handleAgentSmoke(req, started);
21206
21366
  break;
21367
+ case "agent_status":
21368
+ resp = this.handleAgentStatus(req, started);
21369
+ break;
21370
+ case "agent_schedule":
21371
+ resp = this.handleAgentSchedule(req, started);
21372
+ break;
21207
21373
  case "config_propose_edit":
21208
21374
  resp = await this.handleConfigProposeEdit(req, caller, started);
21209
21375
  break;
@@ -21256,6 +21422,12 @@ class HostdServer {
21256
21422
  return callerAdmin ? null : `${req.op} cross-agent requires admin: true on caller "${caller.name}"`;
21257
21423
  case "doctor":
21258
21424
  return callerAdmin ? null : `doctor requires admin: true on caller "${caller.name}"`;
21425
+ case "agent_status":
21426
+ case "agent_schedule":
21427
+ if (req.args.name !== undefined && req.args.name === caller.name) {
21428
+ return null;
21429
+ }
21430
+ return callerAdmin ? null : `${req.op} ${req.args.name === undefined ? "fleet view" : "cross-agent"} requires admin: true on caller "${caller.name}"`;
21259
21431
  case "config_propose_edit":
21260
21432
  return null;
21261
21433
  }
@@ -21726,10 +21898,67 @@ class HostdServer {
21726
21898
  }));
21727
21899
  return respond("running", probes);
21728
21900
  }
21901
+ handleAgentStatus(req, started) {
21902
+ try {
21903
+ const cfg = loadConfig(this.opts.configPath);
21904
+ const all = getAllAgentStatuses(cfg);
21905
+ const statuses = req.args.name ? req.args.name in all ? { [req.args.name]: all[req.args.name] } : {} : all;
21906
+ return {
21907
+ v: 1,
21908
+ request_id: req.request_id,
21909
+ result: "completed",
21910
+ exit_code: 0,
21911
+ duration_ms: Date.now() - started,
21912
+ payload: JSON.stringify({ statuses })
21913
+ };
21914
+ } catch (e) {
21915
+ return {
21916
+ v: 1,
21917
+ request_id: req.request_id,
21918
+ result: "error",
21919
+ exit_code: null,
21920
+ duration_ms: Date.now() - started,
21921
+ error: `agent_status failed: ${e.message}`
21922
+ };
21923
+ }
21924
+ }
21925
+ handleAgentSchedule(req, started) {
21926
+ try {
21927
+ const cfg = loadConfig(this.opts.configPath);
21928
+ let entries = collectScheduleEntries(cfg);
21929
+ if (req.args.name)
21930
+ entries = entries.filter((e) => e.agent === req.args.name);
21931
+ const agentsDir = resolveAgentsDir(cfg);
21932
+ const recentByAgent = {};
21933
+ for (const agent of new Set(entries.map((e) => e.agent))) {
21934
+ const rows = readRecentFires(resolve5(agentsDir, agent, "scheduler.jsonl"));
21935
+ if (rows.length > 0)
21936
+ recentByAgent[agent] = rows;
21937
+ }
21938
+ const bounded = boundScheduleView(entries, recentByAgent);
21939
+ return {
21940
+ v: 1,
21941
+ request_id: req.request_id,
21942
+ result: "completed",
21943
+ exit_code: 0,
21944
+ duration_ms: Date.now() - started,
21945
+ payload: JSON.stringify(bounded)
21946
+ };
21947
+ } catch (e) {
21948
+ return {
21949
+ v: 1,
21950
+ request_id: req.request_id,
21951
+ result: "error",
21952
+ exit_code: null,
21953
+ duration_ms: Date.now() - started,
21954
+ error: `agent_schedule failed: ${e.message}`
21955
+ };
21956
+ }
21957
+ }
21729
21958
  runDocker(args) {
21730
21959
  return new Promise((resolve6, reject) => {
21731
21960
  const bin = this.opts.dockerBin ?? "docker";
21732
- const child = spawn(bin, args, {
21961
+ const child = spawn2(bin, args, {
21733
21962
  stdio: ["ignore", "pipe", "pipe"],
21734
21963
  env: { ...process.env }
21735
21964
  });
@@ -21752,7 +21981,7 @@ class HostdServer {
21752
21981
  const composePath = join3(this.opts.bindRoot ?? this.opts.homeDir, ".switchroom", "compose", "docker-compose.yml");
21753
21982
  if (!existsSync7(composePath))
21754
21983
  return [];
21755
- const r = spawnSync2("docker", [
21984
+ const r = spawnSync3("docker", [
21756
21985
  "compose",
21757
21986
  "-p",
21758
21987
  "switchroom",
@@ -21895,7 +22124,7 @@ class HostdServer {
21895
22124
  runSwitchroom(args) {
21896
22125
  return new Promise((resolve6, reject) => {
21897
22126
  const bin = this.opts.switchroomBin ?? "switchroom";
21898
- const child = spawn(bin, args, {
22127
+ const child = spawn2(bin, args, {
21899
22128
  stdio: ["ignore", "pipe", "pipe"],
21900
22129
  env: { ...process.env }
21901
22130
  });
@@ -21917,6 +22146,49 @@ function tail(s, bytes = TAIL_BYTES) {
21917
22146
  return s;
21918
22147
  return s.slice(s.length - bytes);
21919
22148
  }
22149
+ var SCHEDULE_PROMPT_MAX_CHARS = 160;
22150
+ var SCHEDULE_OUTPUT_SUMMARY_MAX_CHARS = 100;
22151
+ var SCHEDULE_MAX_FIRES_PER_AGENT = 8;
22152
+ var SCHEDULE_FRAME_BUDGET_BYTES = MAX_FRAME_BYTES - 2048;
22153
+ function slimScheduleEntry(e) {
22154
+ const slim = {
22155
+ agent: e.agent,
22156
+ scheduleIndex: e.scheduleIndex,
22157
+ cron: e.cron,
22158
+ promptKey: e.promptKey
22159
+ };
22160
+ if (e.prompt !== undefined) {
22161
+ slim.prompt = e.prompt.length > SCHEDULE_PROMPT_MAX_CHARS ? e.prompt.slice(0, SCHEDULE_PROMPT_MAX_CHARS) : e.prompt;
22162
+ }
22163
+ if (e.kind !== undefined)
22164
+ slim.kind = e.kind;
22165
+ if (e.model !== undefined)
22166
+ slim.model = e.model;
22167
+ if (e.context !== undefined)
22168
+ slim.context = e.context;
22169
+ if (e.topic !== undefined)
22170
+ slim.topic = e.topic;
22171
+ return slim;
22172
+ }
22173
+ function scheduleFrameBytes(view) {
22174
+ return Buffer.byteLength(JSON.stringify(JSON.stringify(view)), "utf8");
22175
+ }
22176
+ function boundScheduleView(entries, recentByAgent) {
22177
+ const slimEntries = entries.map(slimScheduleEntry);
22178
+ const boundedRecent = {};
22179
+ for (const [agent, rows] of Object.entries(recentByAgent)) {
22180
+ boundedRecent[agent] = rows.slice(-SCHEDULE_MAX_FIRES_PER_AGENT).map((r) => typeof r.outputSummary === "string" && r.outputSummary.length > SCHEDULE_OUTPUT_SUMMARY_MAX_CHARS ? { ...r, outputSummary: r.outputSummary.slice(0, SCHEDULE_OUTPUT_SUMMARY_MAX_CHARS) } : { ...r });
22181
+ }
22182
+ let view = { entries: slimEntries, recentByAgent: boundedRecent };
22183
+ if (scheduleFrameBytes(view) <= SCHEDULE_FRAME_BUDGET_BYTES)
22184
+ return view;
22185
+ view = { entries: slimEntries, recentByAgent: {}, truncated: true };
22186
+ while (scheduleFrameBytes(view) > SCHEDULE_FRAME_BUDGET_BYTES && view.entries.length > 1) {
22187
+ const keep = Math.max(1, Math.floor(view.entries.length * 0.8));
22188
+ view = { entries: view.entries.slice(0, keep), recentByAgent: {}, truncated: true };
22189
+ }
22190
+ return view;
22191
+ }
21920
22192
  var READONLY_EXEC_ALLOWLIST = [
21921
22193
  "cat",
21922
22194
  "df",
@@ -22199,10 +22471,10 @@ function errMsg(err2) {
22199
22471
  }
22200
22472
 
22201
22473
  // src/host-control/release-watcher-shellouts.ts
22202
- import { spawn as spawn2 } from "node:child_process";
22474
+ import { spawn as spawn3 } from "node:child_process";
22203
22475
  async function run(cmd, args, timeoutMs) {
22204
22476
  return new Promise((res) => {
22205
- const p = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
22477
+ const p = spawn3(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
22206
22478
  let stdout = "";
22207
22479
  let stderr = "";
22208
22480
  const to = setTimeout(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.15.25",
3
+ "version": "0.15.27",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,12 +27,13 @@
27
27
  "test:vitest": "vitest run",
28
28
  "test:bun": "bun test src/watchdog/state.test.ts src/watchdog/policy.test.ts src/vault/grants.test.ts src/vault/write-grants.test.ts src/vault/broker/server-grants.test.ts src/vault/broker/server-write-grants.test.ts src/vault/broker/server-mint-grant-passphrase-attest.test.ts src/vault/broker/server-passphrase-attest.test.ts src/vault/broker/server-mint-grant-posture-attest.test.ts src/vault/broker/server-admin-only-keys.test.ts src/vault/broker/client-token.test.ts src/vault/broker/server-unlock.test.ts src/vault/broker/auto-unlock.test.ts src/vault/broker/drift-detection.test.ts tests/vault-broker-passphrase.test.ts src/cli/vault-get-broker.test.ts src/vault/resolver-via-broker.test.ts src/vault/broker/scope.test.ts src/vault/broker/server.test.ts src/drive/disconnect.test.ts src/drive/grants.test.ts src/drive/oauth.test.ts src/drive/onboarding.test.ts src/drive/reconciler.test.ts src/drive/vault-slots.test.ts src/drive/wrapper.test.ts src/vault/approvals/kernel.test.ts src/vault/approvals/approval-origin.test.ts src/vault/approvals/schema-idempotent.test.ts src/vault/broker/server-approvals.test.ts telegram-plugin/tests/boot-probes.test.ts telegram-plugin/tests/boot-version-string.test.ts telegram-plugin/tests/history.test.ts telegram-plugin/tests/history-reaper.test.ts telegram-plugin/tests/ipc-server-client.test.ts telegram-plugin/tests/ipc-server-race.test.ts telegram-plugin/tests/gateway-bridge.test.ts telegram-plugin/tests/gateway-startup-mutex.test.ts telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts telegram-plugin/tests/boot-card-dedupe.test.ts telegram-plugin/tests/boot-card-reason.test.ts telegram-plugin/tests/progress-update.test.ts telegram-plugin/tests/quota-cache.test.ts telegram-plugin/tests/silent-reply-guard.test.ts telegram-plugin/tests/unhandled-rejection-policy.test.ts telegram-plugin/tests/registry-turns.test.ts telegram-plugin/registry/subagents.test.ts telegram-plugin/registry/subagents-bugs.test.ts telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts telegram-plugin/tests/turns-writer.test.ts telegram-plugin/tests/resume-inbound-builder.test.ts telegram-plugin/tests/subagent-tracker-hooks.test.ts telegram-plugin/tests/resolve-calling-subagent.test.ts telegram-plugin/tests/gateway-update-placeholder-dispatch.test.ts telegram-plugin/tests/reaction-trigger.test.ts telegram-plugin/tests/reaction-trigger-flow.test.ts telegram-plugin/uat/load-env.test.ts telegram-plugin/uat/feed-matcher.test.ts telegram-plugin/gateway/webhook-ingest-server.test.ts",
29
29
  "test:watch": "vitest",
30
- "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs",
30
+ "lint": "tsc --noEmit && node scripts/check-plugin-references.mjs && bash scripts/check-bot-api-wrapping.sh && node scripts/check-bun-test-imports.mjs && node scripts/check-no-pii-secrets.mjs && node scripts/check-vault-test-hermeticity.mjs && node scripts/check-no-broadcast-delivery.mjs && node scripts/check-stale-tool-descriptions.mjs && node scripts/check-web-subscription-honest.mjs",
31
31
  "lint:tsc": "tsc --noEmit",
32
32
  "lint:plugin-references": "node scripts/check-plugin-references.mjs",
33
33
  "lint:bot-api-wrapping": "bash scripts/check-bot-api-wrapping.sh",
34
34
  "lint:bun-test-imports": "node scripts/check-bun-test-imports.mjs",
35
35
  "lint:no-pii": "node scripts/check-no-pii-secrets.mjs",
36
+ "lint:web-subscription-honest": "node scripts/check-web-subscription-honest.mjs",
36
37
  "lint:no-broadcast-delivery": "node scripts/check-no-broadcast-delivery.mjs",
37
38
  "prepublishOnly": "npm run build && npm run lint && npm test"
38
39
  },
@@ -46442,6 +46442,20 @@ var DoctorRequestSchema = exports_external.object({
46442
46442
  op: exports_external.literal("doctor"),
46443
46443
  args: exports_external.object({}).optional()
46444
46444
  });
46445
+ var AgentStatusRequestSchema = exports_external.object({
46446
+ ...RequestEnvelope,
46447
+ op: exports_external.literal("agent_status"),
46448
+ args: exports_external.object({
46449
+ name: AgentNameSchema2.optional()
46450
+ })
46451
+ });
46452
+ var AgentScheduleRequestSchema = exports_external.object({
46453
+ ...RequestEnvelope,
46454
+ op: exports_external.literal("agent_schedule"),
46455
+ args: exports_external.object({
46456
+ name: AgentNameSchema2.optional()
46457
+ })
46458
+ });
46445
46459
  var AgentSmokeRequestSchema = exports_external.object({
46446
46460
  ...RequestEnvelope,
46447
46461
  op: exports_external.literal("agent_smoke"),
@@ -46472,6 +46486,8 @@ var RequestSchema3 = exports_external.discriminatedUnion("op", [
46472
46486
  AgentExecRequestSchema,
46473
46487
  DoctorRequestSchema,
46474
46488
  AgentSmokeRequestSchema,
46489
+ AgentStatusRequestSchema,
46490
+ AgentScheduleRequestSchema,
46475
46491
  ConfigProposeEditRequestSchema
46476
46492
  ]);
46477
46493
  var ResultSchema = exports_external.enum(["started", "completed", "denied", "error"]);
@@ -46523,6 +46539,7 @@ var ResponseEnvelope = {
46523
46539
  audit_id: exports_external.string().min(1).optional(),
46524
46540
  stdout_tail: exports_external.string().optional(),
46525
46541
  stderr_tail: exports_external.string().optional(),
46542
+ payload: exports_external.string().optional(),
46526
46543
  error: exports_external.string().optional(),
46527
46544
  error_envelope: ErrorEnvelopeSchema.optional()
46528
46545
  };
@@ -54424,10 +54441,10 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
54424
54441
  }
54425
54442
 
54426
54443
  // ../src/build-info.ts
54427
- var VERSION = "0.15.25";
54428
- var COMMIT_SHA = "0d066743";
54429
- var COMMIT_DATE = "2026-06-15T02:15:36Z";
54430
- var LATEST_PR = 2359;
54444
+ var VERSION = "0.15.27";
54445
+ var COMMIT_SHA = "97160057";
54446
+ var COMMIT_DATE = "2026-06-15T06:43:23Z";
54447
+ var LATEST_PR = 2372;
54431
54448
  var COMMITS_AHEAD_OF_TAG = 0;
54432
54449
 
54433
54450
  // gateway/boot-version.ts
@@ -54720,6 +54737,112 @@ async function revokeGrantViaBroker(id, opts) {
54720
54737
 
54721
54738
  // gateway/linear-activity.ts
54722
54739
  init_client2();
54740
+
54741
+ // ../src/linear/oauth-refresh.ts
54742
+ var LINEAR_TOKEN_ENDPOINT = "https://api.linear.app/oauth/token";
54743
+ var DEFAULT_REFRESH_SKEW_SEC = 2 * 3600;
54744
+ async function refreshLinearAppToken(bundle, opts = {}) {
54745
+ const fetchImpl = opts.fetchImpl ?? fetch;
54746
+ const nowSec = opts.nowSec ?? (() => Math.floor(Date.now() / 1000));
54747
+ const form = new URLSearchParams({
54748
+ grant_type: "refresh_token",
54749
+ refresh_token: bundle.refreshToken,
54750
+ client_id: bundle.clientId,
54751
+ client_secret: bundle.clientSecret
54752
+ });
54753
+ let resp;
54754
+ try {
54755
+ resp = await fetchImpl(LINEAR_TOKEN_ENDPOINT, {
54756
+ method: "POST",
54757
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
54758
+ body: form.toString()
54759
+ });
54760
+ } catch (err) {
54761
+ return { ok: false, reason: "network", detail: err.message };
54762
+ }
54763
+ if (!resp.ok) {
54764
+ const txt = await resp.text().catch(() => "");
54765
+ const revoked = resp.status === 400 || /invalid_grant|invalid_token/i.test(txt);
54766
+ return {
54767
+ ok: false,
54768
+ reason: revoked ? "revoked" : "http_error",
54769
+ detail: `HTTP ${resp.status}${txt ? ` ${txt.slice(0, 200)}` : ""}`
54770
+ };
54771
+ }
54772
+ let json;
54773
+ try {
54774
+ json = await resp.json();
54775
+ } catch {
54776
+ return { ok: false, reason: "bad_response", detail: "non-JSON token response" };
54777
+ }
54778
+ const accessToken = json.access_token;
54779
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
54780
+ return { ok: false, reason: "bad_response", detail: "no access_token in response" };
54781
+ }
54782
+ const expiresIn = typeof json.expires_in === "number" ? json.expires_in : 86400;
54783
+ const rotated = typeof json.refresh_token === "string" && json.refresh_token.length > 0 ? json.refresh_token : bundle.refreshToken;
54784
+ return {
54785
+ ok: true,
54786
+ accessToken,
54787
+ refreshToken: rotated,
54788
+ expiresAt: nowSec() + expiresIn,
54789
+ ...typeof json.scope === "string" ? { scope: json.scope } : {}
54790
+ };
54791
+ }
54792
+ function parseBundle(raw) {
54793
+ if (raw == null || raw === "")
54794
+ return null;
54795
+ let o;
54796
+ try {
54797
+ o = JSON.parse(raw);
54798
+ } catch {
54799
+ return null;
54800
+ }
54801
+ if (typeof o.client_id === "string" && typeof o.client_secret === "string" && typeof o.refresh_token === "string" && o.client_id.length > 0 && o.client_secret.length > 0 && o.refresh_token.length > 0) {
54802
+ return {
54803
+ clientId: o.client_id,
54804
+ clientSecret: o.client_secret,
54805
+ refreshToken: o.refresh_token,
54806
+ ...typeof o.expires_at === "number" ? { expiresAt: o.expires_at } : {}
54807
+ };
54808
+ }
54809
+ return null;
54810
+ }
54811
+ function serializeBundle(b) {
54812
+ return JSON.stringify({
54813
+ client_id: b.clientId,
54814
+ client_secret: b.clientSecret,
54815
+ refresh_token: b.refreshToken,
54816
+ ...b.expiresAt != null ? { expires_at: b.expiresAt } : {}
54817
+ });
54818
+ }
54819
+ async function performLinearRefresh(io) {
54820
+ const raw = await io.readBundle();
54821
+ const bundle = parseBundle(raw);
54822
+ if (!bundle) {
54823
+ return { ok: false, reason: "no_bundle", detail: "no/invalid refresh bundle" };
54824
+ }
54825
+ const res = await refreshLinearAppToken(bundle, {
54826
+ ...io.fetchImpl ? { fetchImpl: io.fetchImpl } : {},
54827
+ ...io.nowSec ? { nowSec: io.nowSec } : {}
54828
+ });
54829
+ if (!res.ok)
54830
+ return { ok: false, reason: res.reason, detail: res.detail };
54831
+ try {
54832
+ await io.writeBundle(serializeBundle({
54833
+ clientId: bundle.clientId,
54834
+ clientSecret: bundle.clientSecret,
54835
+ refreshToken: res.refreshToken,
54836
+ expiresAt: res.expiresAt
54837
+ }));
54838
+ await io.writeToken(res.accessToken);
54839
+ } catch (err) {
54840
+ return { ok: false, reason: "persist_failed", detail: err.message };
54841
+ }
54842
+ return { ok: true, accessToken: res.accessToken, expiresAt: res.expiresAt };
54843
+ }
54844
+
54845
+ // gateway/linear-activity.ts
54723
54846
  var LINEAR_GRAPHQL_ENDPOINT = "https://api.linear.app/graphql";
54724
54847
  async function defaultResolveLinearToken(agent) {
54725
54848
  const key = `linear/${agent}/token`;
@@ -54736,6 +54859,53 @@ async function defaultResolveLinearToken(agent) {
54736
54859
  return { ok: false, reason: "denied" };
54737
54860
  return { ok: false, reason: "unknown" };
54738
54861
  }
54862
+ function brokerRefreshIO(agent, fetchImpl) {
54863
+ const token = readVaultTokenFile(agent) ?? undefined;
54864
+ const opt = token ? { token } : {};
54865
+ return {
54866
+ readBundle: async () => {
54867
+ const r = await getViaBrokerStructured(`linear/${agent}/oauth`, opt);
54868
+ return r.kind === "ok" && r.entry.kind === "string" ? r.entry.value : null;
54869
+ },
54870
+ writeToken: async (t) => {
54871
+ const r = await putViaBroker(`linear/${agent}/token`, { kind: "string", value: t }, opt);
54872
+ if (r.kind !== "ok")
54873
+ throw new Error(`broker put linear/${agent}/token: ${r.kind}`);
54874
+ },
54875
+ writeBundle: async (j) => {
54876
+ const r = await putViaBroker(`linear/${agent}/oauth`, { kind: "string", value: j }, opt);
54877
+ if (r.kind !== "ok")
54878
+ throw new Error(`broker put linear/${agent}/oauth: ${r.kind}`);
54879
+ },
54880
+ ...fetchImpl ? { fetchImpl } : {}
54881
+ };
54882
+ }
54883
+ async function linearPostWithRefresh(body, token, agent, fetchImpl, log, refreshIO) {
54884
+ const post = (t) => fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
54885
+ method: "POST",
54886
+ headers: { "Content-Type": "application/json", Authorization: t },
54887
+ body
54888
+ });
54889
+ let resp = await post(token);
54890
+ if (resp.status !== 401)
54891
+ return { resp, token };
54892
+ const io = (refreshIO ?? ((a) => brokerRefreshIO(a, fetchImpl)))(agent);
54893
+ const refreshed = await performLinearRefresh({ ...io, fetchImpl });
54894
+ if (!refreshed.ok) {
54895
+ if (refreshed.reason === "revoked") {
54896
+ log(`telegram gateway: linear token REVOKED agent=${agent} \u2014 refresh token is dead; ` + `operator must re-authorize (linear-agent setup --refresh-token \u2026)
54897
+ `);
54898
+ } else {
54899
+ log(`telegram gateway: linear token refresh failed agent=${agent} reason=${refreshed.reason}
54900
+ `);
54901
+ }
54902
+ return { resp, token };
54903
+ }
54904
+ log(`telegram gateway: linear token auto-refreshed agent=${agent} (was 401)
54905
+ `);
54906
+ resp = await post(refreshed.accessToken);
54907
+ return { resp, token: refreshed.accessToken };
54908
+ }
54739
54909
  async function emitLinearAgentActivity(args, deps = {}) {
54740
54910
  const log = deps.log ?? ((s) => process.stderr.write(s));
54741
54911
  const sessionId = args.agent_session_id;
@@ -54780,14 +54950,7 @@ async function emitLinearAgentActivity(args, deps = {}) {
54780
54950
  const fetchImpl = deps.fetchImpl ?? fetch;
54781
54951
  let resp;
54782
54952
  try {
54783
- resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
54784
- method: "POST",
54785
- headers: {
54786
- "Content-Type": "application/json",
54787
- Authorization: tokenResult.token
54788
- },
54789
- body: JSON.stringify({ query: mutation, variables })
54790
- });
54953
+ ({ resp } = await linearPostWithRefresh(JSON.stringify({ query: mutation, variables }), tokenResult.token, agent, fetchImpl, log, deps.refreshIO));
54791
54954
  } catch (err) {
54792
54955
  return {
54793
54956
  content: [{ type: "text", text: `linear_agent_activity failed: request error: ${err.message}` }]
@@ -54847,15 +55010,13 @@ async function createLinearIssue(args, deps = {}) {
54847
55010
  ]
54848
55011
  };
54849
55012
  }
54850
- const token = tokenResult.token;
55013
+ let activeToken = tokenResult.token;
54851
55014
  const gql = async (query2, variables) => {
54852
55015
  let resp;
54853
55016
  try {
54854
- resp = await fetchImpl(LINEAR_GRAPHQL_ENDPOINT, {
54855
- method: "POST",
54856
- headers: { "Content-Type": "application/json", Authorization: token },
54857
- body: JSON.stringify({ query: query2, variables })
54858
- });
55017
+ const out = await linearPostWithRefresh(JSON.stringify({ query: query2, variables }), activeToken, agent, fetchImpl, log, deps.refreshIO);
55018
+ resp = out.resp;
55019
+ activeToken = out.token;
54859
55020
  } catch (err) {
54860
55021
  return { ok: false, text: `request error: ${err.message}` };
54861
55022
  }