@n1creator/openacp-cli 2026.712.9 → 2026.712.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/index.js CHANGED
@@ -458,22 +458,22 @@ __export(config_registry_exports, {
458
458
  resolveOptions: () => resolveOptions,
459
459
  setFieldValueAsync: () => setFieldValueAsync
460
460
  });
461
- function getFieldDef(path37) {
462
- return CONFIG_REGISTRY.find((f) => f.path === path37);
461
+ function getFieldDef(path38) {
462
+ return CONFIG_REGISTRY.find((f) => f.path === path38);
463
463
  }
464
464
  function getSafeFields() {
465
465
  return CONFIG_REGISTRY.filter((f) => f.scope === "safe");
466
466
  }
467
- function isHotReloadable(path37) {
468
- const def = getFieldDef(path37);
467
+ function isHotReloadable(path38) {
468
+ const def = getFieldDef(path38);
469
469
  return def?.hotReload ?? false;
470
470
  }
471
471
  function resolveOptions(def, config) {
472
472
  if (!def.options) return void 0;
473
473
  return typeof def.options === "function" ? def.options(config) : def.options;
474
474
  }
475
- function getConfigValue(config, path37) {
476
- const parts = path37.split(".");
475
+ function getConfigValue(config, path38) {
476
+ const parts = path38.split(".");
477
477
  let current = config;
478
478
  for (const part of parts) {
479
479
  if (current && typeof current === "object" && part in current) {
@@ -1546,7 +1546,7 @@ async function downloadAndExtract(agentId, archiveUrl, progress, agentsDir, scop
1546
1546
  const destDir = path10.join(agentsDir ?? DEFAULT_AGENTS_DIR, agentId);
1547
1547
  fs11.mkdirSync(destDir, { recursive: true });
1548
1548
  await progress?.onStep("Downloading...");
1549
- log11.info({ agentId, url: archiveUrl }, "Downloading agent binary");
1549
+ log12.info({ agentId, url: archiveUrl }, "Downloading agent binary");
1550
1550
  const response = await scopedFetch(archiveUrl);
1551
1551
  if (!response.ok) {
1552
1552
  throw new Error(`Download failed: ${response.status} ${response.statusText}`);
@@ -1656,17 +1656,17 @@ async function uninstallAgent(agentKey, store, agentsDir) {
1656
1656
  if (agent.binaryPath && fs11.existsSync(agent.binaryPath)) {
1657
1657
  validateUninstallPath(agent.binaryPath, agentsDir ?? DEFAULT_AGENTS_DIR);
1658
1658
  fs11.rmSync(agent.binaryPath, { recursive: true, force: true });
1659
- log11.info({ agentKey, binaryPath: agent.binaryPath }, "Deleted agent binary");
1659
+ log12.info({ agentKey, binaryPath: agent.binaryPath }, "Deleted agent binary");
1660
1660
  }
1661
1661
  store.removeAgent(agentKey);
1662
1662
  }
1663
- var log11, DEFAULT_AGENTS_DIR, MAX_DOWNLOAD_SIZE, validateTarContents, ARCH_MAP, PLATFORM_MAP;
1663
+ var log12, DEFAULT_AGENTS_DIR, MAX_DOWNLOAD_SIZE, validateTarContents, ARCH_MAP, PLATFORM_MAP;
1664
1664
  var init_agent_installer = __esm({
1665
1665
  "src/core/agents/agent-installer.ts"() {
1666
1666
  "use strict";
1667
1667
  init_log();
1668
1668
  init_agent_dependencies();
1669
- log11 = createChildLogger({ module: "agent-installer" });
1669
+ log12 = createChildLogger({ module: "agent-installer" });
1670
1670
  DEFAULT_AGENTS_DIR = path10.join(os4.homedir(), ".openacp", "agents");
1671
1671
  MAX_DOWNLOAD_SIZE = 500 * 1024 * 1024;
1672
1672
  validateTarContents = validateArchiveContents;
@@ -2242,7 +2242,7 @@ function copyResponse(response, body) {
2242
2242
  }
2243
2243
  return copy;
2244
2244
  }
2245
- var enabledDebug, PROXY_ENV_KEYS, ProxyValidationError, ProxyUnknownScopeError, ProxyProfileInUseError, ProxyRouteTestError, ProxyProfileTestError, ProxyService;
2245
+ var enabledDebug, PROXY_ENV_KEYS, ProxyValidationError, ProxyUnknownScopeError, ProxyProfileInUseError, ProxyProfileNotFoundError, ProxyProfileExistsError, ProxyRouteTestError, ProxyProfileTestError, ProxyService;
2246
2246
  var init_proxy_service = __esm({
2247
2247
  "src/core/network/proxy-service.ts"() {
2248
2248
  "use strict";
@@ -2281,6 +2281,16 @@ var init_proxy_service = __esm({
2281
2281
  super(`Proxy profile "${id}" is still used by routing`, "PROXY_PROFILE_IN_USE");
2282
2282
  }
2283
2283
  };
2284
+ ProxyProfileNotFoundError = class extends ProxyValidationError {
2285
+ constructor(id) {
2286
+ super(`Proxy profile "${id}" does not exist`, "PROXY_PROFILE_NOT_FOUND");
2287
+ }
2288
+ };
2289
+ ProxyProfileExistsError = class extends ProxyValidationError {
2290
+ constructor(id) {
2291
+ super(`Proxy profile "${id}" already exists`, "PROXY_PROFILE_EXISTS");
2292
+ }
2293
+ };
2284
2294
  ProxyRouteTestError = class extends Error {
2285
2295
  code = "PROXY_ROUTE_TEST_FAILED";
2286
2296
  constructor(scope, cause) {
@@ -2390,13 +2400,28 @@ var init_proxy_service = __esm({
2390
2400
  return profile;
2391
2401
  }
2392
2402
  buildProfile(input2, config, secrets) {
2403
+ input2 = this.normalizeProfileInput(input2);
2393
2404
  validateId(input2.id, "profile id");
2394
- if (!PROXY_PROTOCOLS.includes(input2.protocol)) throw new ProxyValidationError(`Unsupported proxy protocol: ${input2.protocol}`);
2405
+ if (input2.name !== void 0) {
2406
+ const name = input2.name.trim();
2407
+ if (!name || name.length > 100) throw new ProxyValidationError("Proxy profile name must contain 1-100 non-whitespace characters");
2408
+ input2 = { ...input2, name };
2409
+ }
2410
+ if (!input2.protocol || !PROXY_PROTOCOLS.includes(input2.protocol)) throw new ProxyValidationError(`Unsupported proxy protocol: ${input2.protocol}`);
2395
2411
  if (!Number.isInteger(input2.port) || input2.port < 1 || input2.port > 65535) throw new ProxyValidationError("Proxy port must be between 1 and 65535");
2396
- const host = canonicalHost(input2.host);
2412
+ const host = canonicalHost(input2.host ?? "");
2413
+ for (const [label, value] of [["username", input2.username], ["password", input2.password]]) {
2414
+ if (value !== void 0 && (value.length > 4096 || /[\r\n\u0000]/.test(value))) {
2415
+ throw new ProxyValidationError(`Proxy ${label} contains unsupported control characters or exceeds 4096 characters`);
2416
+ }
2417
+ }
2418
+ if (input2.noProxy && (input2.noProxy.length > 256 || input2.noProxy.some((item) => !item || /[\u0000-\u0020\u007f]/.test(item)))) {
2419
+ throw new ProxyValidationError("NO_PROXY entries must be non-empty, whitespace-free values (maximum 256 entries)");
2420
+ }
2397
2421
  const existing = config.profiles.find((p) => p.id === input2.id);
2398
2422
  const nextSecrets = structuredClone(secrets);
2399
- if (input2.username !== void 0 || input2.password !== void 0) nextSecrets[input2.id] = { username: input2.username ?? nextSecrets[input2.id]?.username, password: input2.password ?? nextSecrets[input2.id]?.password };
2423
+ if (input2.clearCredentials) delete nextSecrets[input2.id];
2424
+ else if (input2.username !== void 0 || input2.password !== void 0) nextSecrets[input2.id] = { username: input2.username ?? nextSecrets[input2.id]?.username, password: input2.password ?? nextSecrets[input2.id]?.password };
2400
2425
  const secret = nextSecrets[input2.id];
2401
2426
  return { profile: {
2402
2427
  id: input2.id,
@@ -2409,11 +2434,68 @@ var init_proxy_service = __esm({
2409
2434
  hasCredentials: Boolean(secret?.username || secret?.password)
2410
2435
  }, nextSecrets };
2411
2436
  }
2437
+ /** Parse a write-only proxy URL into canonical components without retaining it. */
2438
+ parseProxyUrlInput(proxyUrl2) {
2439
+ if (!proxyUrl2 || /[\u0000-\u001f\u007f]/.test(proxyUrl2)) throw new ProxyValidationError("Proxy URL contains invalid control characters");
2440
+ let url;
2441
+ try {
2442
+ url = new URL(proxyUrl2);
2443
+ } catch {
2444
+ throw new ProxyValidationError("Proxy URL is invalid");
2445
+ }
2446
+ const protocol = url.protocol.slice(0, -1);
2447
+ if (!PROXY_PROTOCOLS.includes(protocol)) throw new ProxyValidationError(`Unsupported proxy protocol: ${protocol}`);
2448
+ if (url.pathname && url.pathname !== "/" || url.search || url.hash) throw new ProxyValidationError("Proxy URL must not contain a path, query, or fragment");
2449
+ const authority = /^[a-z][a-z0-9+.-]*:\/\/([^/?#]*)/i.exec(proxyUrl2)?.[1];
2450
+ const hostPort = authority?.slice(authority.lastIndexOf("@") + 1 || 0);
2451
+ const explicitPort = hostPort?.startsWith("[") ? /^\[[^\]]+\]:(\d+)$/.exec(hostPort)?.[1] : /:(\d+)$/.exec(hostPort ?? "")?.[1];
2452
+ if (!explicitPort) throw new ProxyValidationError("Proxy URL must include an explicit port");
2453
+ let username = "";
2454
+ let password2 = "";
2455
+ try {
2456
+ username = decodeURIComponent(url.username);
2457
+ password2 = decodeURIComponent(url.password);
2458
+ } catch {
2459
+ throw new ProxyValidationError("Proxy URL credentials contain invalid percent encoding");
2460
+ }
2461
+ return {
2462
+ protocol,
2463
+ host: url.hostname,
2464
+ port: Number(url.port || explicitPort),
2465
+ ...username ? { username } : {},
2466
+ ...password2 ? { password: password2 } : {},
2467
+ // Replacing an existing endpoint with a URL that has no userinfo must
2468
+ // remove the old write-only secret instead of silently retaining it.
2469
+ clearCredentials: !username && !password2
2470
+ };
2471
+ }
2472
+ normalizeProfileInput(input2) {
2473
+ if (input2.proxyUrl !== void 0) {
2474
+ const componentKeys = ["protocol", "host", "port", "username", "password", "clearCredentials"];
2475
+ if (componentKeys.some((key) => input2[key] !== void 0)) throw new ProxyValidationError("proxyUrl is mutually exclusive with endpoint and credential fields");
2476
+ const { proxyUrl: proxyUrl2, ...metadata } = input2;
2477
+ return { ...metadata, ...this.parseProxyUrlInput(proxyUrl2) };
2478
+ }
2479
+ if (!input2.protocol || !input2.host || input2.port === void 0) throw new ProxyValidationError("protocol, host, and port are required when proxyUrl is not provided");
2480
+ return input2;
2481
+ }
2412
2482
  async saveProfileSafely(input2, expectedRevision) {
2483
+ return this.mutateProfileSafely(input2, "upsert", expectedRevision);
2484
+ }
2485
+ async createProfileSafely(input2, expectedRevision) {
2486
+ return this.mutateProfileSafely(input2, "create", expectedRevision);
2487
+ }
2488
+ async updateProfileSafely(input2, expectedRevision) {
2489
+ return this.mutateProfileSafely(input2, "update", expectedRevision);
2490
+ }
2491
+ async mutateProfileSafely(input2, operation, expectedRevision) {
2413
2492
  return this.serialize(async () => {
2414
2493
  const config = this.store.load();
2415
2494
  const secrets = this.store.getSecrets();
2416
2495
  if (expectedRevision !== void 0 && config.revision !== expectedRevision) throw new ProxyRevisionConflictError(expectedRevision, config.revision);
2496
+ const exists = config.profiles.some((profile) => profile.id === input2.id);
2497
+ if (operation === "create" && exists) throw new ProxyProfileExistsError(input2.id);
2498
+ if (operation === "update" && !exists) throw new ProxyProfileNotFoundError(input2.id);
2417
2499
  const { profile: candidate, nextSecrets } = this.buildProfile(input2, config, secrets);
2418
2500
  const affectedScopes = this.scopesUsingProfile(config, input2.id);
2419
2501
  const candidateSecret = nextSecrets[input2.id];
@@ -2431,7 +2513,18 @@ var init_proxy_service = __esm({
2431
2513
  }
2432
2514
  this.invalidatePolicyBeforeCommit(config, input2.id);
2433
2515
  config.profiles = [...config.profiles.filter((p) => p.id !== candidate.id), candidate];
2434
- const saved = this.store.commit(config, nextSecrets, config.revision).profiles.find((p) => p.id === candidate.id);
2516
+ let committed;
2517
+ try {
2518
+ committed = this.store.commit(config, nextSecrets, config.revision);
2519
+ } catch (error) {
2520
+ if (error instanceof ProxyRevisionConflictError && operation !== "upsert") {
2521
+ const latestExists = this.store.load().profiles.some((profile) => profile.id === input2.id);
2522
+ if (operation === "create" && latestExists) throw new ProxyProfileExistsError(input2.id);
2523
+ if (operation === "update" && !latestExists) throw new ProxyProfileNotFoundError(input2.id);
2524
+ }
2525
+ throw error;
2526
+ }
2527
+ const saved = committed.profiles.find((p) => p.id === candidate.id);
2435
2528
  this.retireScopes(affectedScopes);
2436
2529
  return saved;
2437
2530
  });
@@ -2486,15 +2579,37 @@ var init_proxy_service = __esm({
2486
2579
  return this.saveProfileSafely(this.parseEnvFile(id, envFile, name), expectedRevision);
2487
2580
  }
2488
2581
  async deleteProfile(id, expectedRevision) {
2582
+ await this.deleteProfileSafely(id, void 0, expectedRevision);
2583
+ }
2584
+ /** Delete a profile, optionally reassigning every direct reference in one tested CAS transaction. */
2585
+ async deleteProfileSafely(id, reassign, expectedRevision) {
2489
2586
  return this.serialize(async () => {
2490
2587
  const config = this.store.load();
2491
2588
  const secrets = this.store.getSecrets();
2492
2589
  if (expectedRevision !== void 0 && config.revision !== expectedRevision) throw new ProxyRevisionConflictError(expectedRevision, config.revision);
2493
- if (!config.profiles.some((p) => p.id === id)) throw new ProxyValidationError(`Proxy profile "${id}" does not exist`);
2494
- if (Object.values(config.routing.routes).includes(`profile:${id}`) || config.routing.global === `profile:${id}`) throw new ProxyProfileInUseError(id);
2495
- config.profiles = config.profiles.filter((p) => p.id !== id);
2590
+ if (!config.profiles.some((p) => p.id === id)) throw new ProxyProfileNotFoundError(id);
2591
+ const directScopes = Object.entries(config.routing.routes).filter(([, route]) => route === `profile:${id}`).map(([scope]) => scope);
2592
+ const globalUsesProfile = config.routing.global === `profile:${id}`;
2593
+ if ((directScopes.length || globalUsesProfile) && !reassign) throw new ProxyProfileInUseError(id);
2594
+ if (reassign === `profile:${id}`) throw new ProxyValidationError("A deleted profile cannot be reassigned to itself");
2595
+ if (reassign) this.validateRoute(reassign);
2596
+ const candidate = structuredClone(config);
2597
+ if (globalUsesProfile) candidate.routing.global = reassign;
2598
+ for (const scope of directScopes) candidate.routing.routes[scope] = reassign;
2599
+ const changedScopes = this.changedResolutionScopes(config, candidate);
2600
+ try {
2601
+ await this.testCandidateRoutes(config, candidate);
2602
+ } catch (error) {
2603
+ throw new ProxyRouteTestError(globalUsesProfile ? "global" : directScopes[0], error);
2604
+ }
2605
+ candidate.profiles = candidate.profiles.filter((p) => p.id !== id);
2496
2606
  delete secrets[id];
2497
- this.store.commit(config, secrets, config.revision);
2607
+ this.invalidatePolicyBeforeCommit(config, id);
2608
+ this.store.commit(candidate, secrets, config.revision);
2609
+ this.retireScopes(changedScopes);
2610
+ if (globalUsesProfile) for (const listener of this.listeners) await listener("global", reassign);
2611
+ for (const scope of directScopes) for (const listener of this.listeners) await listener(scope, reassign);
2612
+ return { reassignedScopes: [.../* @__PURE__ */ new Set([...globalUsesProfile ? ["global"] : [], ...directScopes])] };
2498
2613
  });
2499
2614
  }
2500
2615
  resolve(scope, routeOverride) {
@@ -2713,6 +2828,27 @@ var init_proxy_service = __esm({
2713
2828
  fetcher?.destroy?.();
2714
2829
  }
2715
2830
  }
2831
+ /** Test an unsaved profile candidate entirely in memory. Credentials never reach persistence. */
2832
+ async testProfileCandidate(input2, targetUrl) {
2833
+ const config = this.store.load();
2834
+ const { profile, nextSecrets } = this.buildProfile(input2, config, this.store.getSecrets());
2835
+ const fetcher = this.createProfileFetch(profile, nextSecrets[profile.id]);
2836
+ let response;
2837
+ try {
2838
+ response = await fetcher(targetUrl ?? "https://api.ipify.org?format=json", {
2839
+ signal: AbortSignal.timeout(1e4)
2840
+ });
2841
+ return { ok: response.ok, status: response.status };
2842
+ } catch (error) {
2843
+ return { ok: false, error: error instanceof Error ? redactNetworkSecrets(error.message) : "Proxy test failed" };
2844
+ } finally {
2845
+ try {
2846
+ await response?.body?.cancel();
2847
+ } catch {
2848
+ }
2849
+ fetcher.destroy?.();
2850
+ }
2851
+ }
2716
2852
  status() {
2717
2853
  const config = this.store.load();
2718
2854
  const scopes = this.getKnownScopes();
@@ -3035,6 +3171,19 @@ var init_agents = __esm({
3035
3171
  }
3036
3172
  });
3037
3173
 
3174
+ // src/core/telegram-command-scopes.ts
3175
+ function effectiveTelegramGroupCommands(lists, administrator) {
3176
+ const candidates = administrator ? [lists.chatAdmins, lists.chat, lists.allAdmins, lists.allGroup, lists.default] : [lists.chat, lists.allGroup, lists.default];
3177
+ return candidates.find((commands) => commands.length > 0) ?? [];
3178
+ }
3179
+ var TELEGRAM_COMMAND_LOCALES;
3180
+ var init_telegram_command_scopes = __esm({
3181
+ "src/core/telegram-command-scopes.ts"() {
3182
+ "use strict";
3183
+ TELEGRAM_COMMAND_LOCALES = ["", "en", "ru"];
3184
+ }
3185
+ });
3186
+
3038
3187
  // src/core/plugin/settings-manager.ts
3039
3188
  var settings_manager_exports = {};
3040
3189
  __export(settings_manager_exports, {
@@ -3150,13 +3299,350 @@ var init_settings_manager = __esm({
3150
3299
  }
3151
3300
  });
3152
3301
 
3302
+ // src/plugins/telegram/command-ownership-store.ts
3303
+ var command_ownership_store_exports = {};
3304
+ __export(command_ownership_store_exports, {
3305
+ TelegramCommandOwnerConflictError: () => TelegramCommandOwnerConflictError,
3306
+ TelegramCommandOwnershipStore: () => TelegramCommandOwnershipStore,
3307
+ telegramCommandHostId: () => telegramCommandHostId,
3308
+ telegramCommandInstanceKey: () => telegramCommandInstanceKey
3309
+ });
3310
+ import fs20 from "fs";
3311
+ import path19 from "path";
3312
+ import os5 from "os";
3313
+ import { createHash as createHash2 } from "crypto";
3314
+ function telegramCommandHostId() {
3315
+ let source = `${os5.hostname()}|${typeof process.getuid === "function" ? process.getuid() : "unknown"}`;
3316
+ try {
3317
+ source = fs20.readFileSync("/etc/machine-id", "utf8").trim() || source;
3318
+ } catch {
3319
+ }
3320
+ return createHash2("sha256").update(source).digest("hex");
3321
+ }
3322
+ function telegramCommandInstanceKey(instanceRoot) {
3323
+ return createHash2("sha256").update(path19.resolve(instanceRoot)).digest("hex");
3324
+ }
3325
+ function emptyLedger() {
3326
+ return { version: STORE_VERSION, bots: {} };
3327
+ }
3328
+ function validateLedger(value) {
3329
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid root");
3330
+ const ledger = value;
3331
+ if (ledger.version !== STORE_VERSION || !ledger.bots || typeof ledger.bots !== "object") throw new Error("invalid version/bots");
3332
+ const botEntries = Object.entries(ledger.bots);
3333
+ if (botEntries.length > MAX_BOTS) throw new Error("too many bots");
3334
+ for (const [botId, bot] of botEntries) {
3335
+ if (!/^\d{1,20}$/.test(botId) || !bot || typeof bot !== "object" || !bot.scopes || typeof bot.scopes !== "object") throw new Error("invalid bot");
3336
+ if (bot.owner) {
3337
+ const owner = bot.owner;
3338
+ if (typeof owner.instanceId !== "string" || !owner.instanceId || owner.instanceId.length > 128 || /[\u0000-\u001f\u007f]/.test(owner.instanceId) || !/^[a-f0-9]{64}$/.test(owner.instanceKey) || !/^[a-f0-9]{64}$/.test(owner.hostId) || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 || !Number.isFinite(Date.parse(owner.heartbeatAt)) || owner.stoppedAt !== void 0 && !Number.isFinite(Date.parse(owner.stoppedAt))) throw new Error("invalid owner");
3339
+ }
3340
+ const scopes = Object.entries(bot.scopes);
3341
+ if (scopes.length > MAX_SCOPES_PER_BOT) throw new Error("too many scopes");
3342
+ for (const [key, scope] of scopes) {
3343
+ if (!/^(default|chat:[a-f0-9]{16}|chat_administrators:[a-f0-9]{16})\|(neutral|en|ru)$/.test(key)) throw new Error("invalid scope key");
3344
+ if (!scope || !Array.isArray(scope.owned) || scope.owned.length > MAX_NAMES_PER_SCOPE) throw new Error("invalid ownership");
3345
+ if (scope.owned.some((name) => typeof name !== "string" || !/^[a-z0-9_]{1,32}$/.test(name))) throw new Error("invalid command name");
3346
+ }
3347
+ }
3348
+ return structuredClone(ledger);
3349
+ }
3350
+ var STORE_VERSION, MAX_BOTS, MAX_SCOPES_PER_BOT, MAX_NAMES_PER_SCOPE, TelegramCommandOwnerConflictError, TelegramCommandOwnershipStore;
3351
+ var init_command_ownership_store = __esm({
3352
+ "src/plugins/telegram/command-ownership-store.ts"() {
3353
+ "use strict";
3354
+ STORE_VERSION = 2;
3355
+ MAX_BOTS = 8;
3356
+ MAX_SCOPES_PER_BOT = 64;
3357
+ MAX_NAMES_PER_SCOPE = 100;
3358
+ TelegramCommandOwnerConflictError = class extends Error {
3359
+ constructor(ownerInstanceId) {
3360
+ super("This Telegram bot command menu is owned by another OpenACP instance. Configure a unique bot per instance, stop the current owner, or perform an explicit same-host takeover.");
3361
+ this.ownerInstanceId = ownerInstanceId;
3362
+ this.name = "TelegramCommandOwnerConflictError";
3363
+ }
3364
+ ownerInstanceId;
3365
+ code = "TELEGRAM_COMMAND_OWNER_CONFLICT";
3366
+ };
3367
+ TelegramCommandOwnershipStore = class {
3368
+ file;
3369
+ lockFile;
3370
+ constructor(instanceRoot) {
3371
+ this.file = path19.join(instanceRoot, "telegram-command-ownership.json");
3372
+ this.lockFile = `${this.file}.lock`;
3373
+ }
3374
+ async withLock(operation) {
3375
+ fs20.mkdirSync(path19.dirname(this.file), { recursive: true, mode: 448 });
3376
+ const deadline = Date.now() + 1e4;
3377
+ let fd;
3378
+ while (fd === void 0) {
3379
+ try {
3380
+ fd = fs20.openSync(this.lockFile, "wx", 384);
3381
+ fs20.writeFileSync(fd, String(process.pid));
3382
+ } catch (error) {
3383
+ if (error.code !== "EEXIST") throw error;
3384
+ try {
3385
+ const owner = Number(fs20.readFileSync(this.lockFile, "utf8"));
3386
+ let alive = Number.isSafeInteger(owner) && owner > 0;
3387
+ if (alive) {
3388
+ try {
3389
+ process.kill(owner, 0);
3390
+ } catch {
3391
+ alive = false;
3392
+ }
3393
+ }
3394
+ if (!alive) fs20.unlinkSync(this.lockFile);
3395
+ } catch {
3396
+ }
3397
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for Telegram command ownership lock");
3398
+ await new Promise((resolve7) => setTimeout(resolve7, 50));
3399
+ }
3400
+ }
3401
+ try {
3402
+ return await operation(this.load());
3403
+ } finally {
3404
+ try {
3405
+ fs20.closeSync(fd);
3406
+ } catch {
3407
+ }
3408
+ try {
3409
+ fs20.unlinkSync(this.lockFile);
3410
+ } catch {
3411
+ }
3412
+ }
3413
+ }
3414
+ load() {
3415
+ if (!fs20.existsSync(this.file)) return { ledger: emptyLedger(), conservative: false };
3416
+ try {
3417
+ return { ledger: validateLedger(JSON.parse(fs20.readFileSync(this.file, "utf8"))), conservative: false };
3418
+ } catch {
3419
+ const quarantine = `${this.file}.corrupt.${Date.now()}`;
3420
+ try {
3421
+ fs20.renameSync(this.file, quarantine);
3422
+ } catch {
3423
+ }
3424
+ return { ledger: emptyLedger(), conservative: true };
3425
+ }
3426
+ }
3427
+ getOwned(ledger, botId, scopeKey) {
3428
+ return ledger.bots[botId]?.scopes[scopeKey]?.owned;
3429
+ }
3430
+ /** Claim command-list ownership while the ledger lock is held. */
3431
+ claimOwner(ledger, botId, identity, allowTakeover = false) {
3432
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3433
+ const bot = this.ensureBot(ledger, botId, now);
3434
+ const current = bot.owner;
3435
+ const sameProcess = current?.hostId === identity.hostId && current.instanceKey === identity.instanceKey && current.pid === identity.pid;
3436
+ if (!current || sameProcess || current.stoppedAt && current.hostId === identity.hostId && current.instanceKey === identity.instanceKey) {
3437
+ bot.owner = { ...identity, heartbeatAt: now };
3438
+ bot.updatedAt = now;
3439
+ return;
3440
+ }
3441
+ const sameHost = current.hostId === identity.hostId;
3442
+ const provablyStopped = sameHost && (Boolean(current.stoppedAt) || !this.processAlive(current.pid));
3443
+ if (allowTakeover && provablyStopped) {
3444
+ bot.owner = { ...identity, heartbeatAt: now };
3445
+ bot.updatedAt = now;
3446
+ return;
3447
+ }
3448
+ throw new TelegramCommandOwnerConflictError(current.instanceId);
3449
+ }
3450
+ async heartbeatOwner(botId, identity) {
3451
+ return this.withLock(async ({ ledger }) => {
3452
+ const bot = ledger.bots[botId];
3453
+ const owner = bot?.owner;
3454
+ if (!owner || owner.hostId !== identity.hostId || owner.instanceKey !== identity.instanceKey || owner.pid !== identity.pid || owner.stoppedAt) return false;
3455
+ owner.heartbeatAt = (/* @__PURE__ */ new Date()).toISOString();
3456
+ bot.updatedAt = owner.heartbeatAt;
3457
+ this.save(ledger);
3458
+ return true;
3459
+ });
3460
+ }
3461
+ async releaseOwner(botId, identity) {
3462
+ await this.withLock(async ({ ledger }) => {
3463
+ const bot = ledger.bots[botId];
3464
+ const owner = bot?.owner;
3465
+ if (!owner || owner.hostId !== identity.hostId || owner.instanceKey !== identity.instanceKey || owner.pid !== identity.pid) return;
3466
+ owner.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
3467
+ owner.heartbeatAt = owner.stoppedAt;
3468
+ bot.updatedAt = owner.stoppedAt;
3469
+ this.save(ledger);
3470
+ });
3471
+ }
3472
+ getOwner(botId) {
3473
+ return this.load().ledger.bots[botId]?.owner;
3474
+ }
3475
+ setOwned(ledger, botId, scopeKey, names) {
3476
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3477
+ const bot = this.ensureBot(ledger, botId, now);
3478
+ bot.updatedAt = now;
3479
+ bot.scopes[scopeKey] = { owned: [...new Set(names)].slice(0, MAX_NAMES_PER_SCOPE), updatedAt: now };
3480
+ }
3481
+ ensureBot(ledger, botId, now) {
3482
+ if (!ledger.bots[botId]) {
3483
+ const entries = Object.entries(ledger.bots).sort((a, b) => a[1].updatedAt.localeCompare(b[1].updatedAt));
3484
+ while (entries.length >= MAX_BOTS) {
3485
+ const oldest = entries.shift();
3486
+ if (oldest) delete ledger.bots[oldest[0]];
3487
+ }
3488
+ ledger.bots[botId] = { scopes: {}, updatedAt: now };
3489
+ }
3490
+ return ledger.bots[botId];
3491
+ }
3492
+ processAlive(pid) {
3493
+ try {
3494
+ process.kill(pid, 0);
3495
+ return true;
3496
+ } catch {
3497
+ return false;
3498
+ }
3499
+ }
3500
+ save(ledger) {
3501
+ const clean = validateLedger(ledger);
3502
+ fs20.mkdirSync(path19.dirname(this.file), { recursive: true, mode: 448 });
3503
+ const tmp = `${this.file}.${process.pid}.${Date.now()}.tmp`;
3504
+ fs20.writeFileSync(tmp, `${JSON.stringify(clean, null, 2)}
3505
+ `, { mode: 384, flag: "wx" });
3506
+ fs20.chmodSync(tmp, 384);
3507
+ const fd = fs20.openSync(tmp, "r");
3508
+ fs20.fsyncSync(fd);
3509
+ fs20.closeSync(fd);
3510
+ fs20.renameSync(tmp, this.file);
3511
+ try {
3512
+ const dir = fs20.openSync(path19.dirname(this.file), "r");
3513
+ fs20.fsyncSync(dir);
3514
+ fs20.closeSync(dir);
3515
+ } catch {
3516
+ }
3517
+ }
3518
+ };
3519
+ }
3520
+ });
3521
+
3522
+ // src/core/instance/instance-context.ts
3523
+ var instance_context_exports = {};
3524
+ __export(instance_context_exports, {
3525
+ createInstanceContext: () => createInstanceContext,
3526
+ generateSlug: () => generateSlug,
3527
+ getGlobalRoot: () => getGlobalRoot,
3528
+ resolveInstanceRoot: () => resolveInstanceRoot,
3529
+ resolveRunningInstance: () => resolveRunningInstance
3530
+ });
3531
+ import path20 from "path";
3532
+ import fs21 from "fs";
3533
+ import os6 from "os";
3534
+ function createInstanceContext(opts) {
3535
+ const { id, root } = opts;
3536
+ const globalRoot = getGlobalRoot();
3537
+ return {
3538
+ id,
3539
+ root,
3540
+ paths: {
3541
+ config: path20.join(root, "config.json"),
3542
+ sessions: path20.join(root, "sessions.json"),
3543
+ agents: path20.join(root, "agents.json"),
3544
+ registryCache: path20.join(globalRoot, "cache", "registry-cache.json"),
3545
+ plugins: path20.join(root, "plugins"),
3546
+ pluginsData: path20.join(root, "plugins", "data"),
3547
+ pluginRegistry: path20.join(root, "plugins.json"),
3548
+ logs: path20.join(root, "logs"),
3549
+ pid: path20.join(root, "openacp.pid"),
3550
+ running: path20.join(root, "running"),
3551
+ apiPort: path20.join(root, "api.port"),
3552
+ apiSecret: path20.join(root, "api-secret"),
3553
+ bin: path20.join(globalRoot, "bin"),
3554
+ cache: path20.join(root, "cache"),
3555
+ tunnels: path20.join(root, "tunnels.json"),
3556
+ agentsDir: path20.join(globalRoot, "agents")
3557
+ }
3558
+ };
3559
+ }
3560
+ function generateSlug(name) {
3561
+ const slug = name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
3562
+ return slug || "openacp";
3563
+ }
3564
+ function expandHome3(p) {
3565
+ if (p.startsWith("~")) return path20.join(os6.homedir(), p.slice(1));
3566
+ return p;
3567
+ }
3568
+ function resolveInstanceRoot(opts) {
3569
+ const cwd = opts.cwd ?? process.cwd();
3570
+ const home = os6.homedir();
3571
+ const globalRoot = getGlobalRoot();
3572
+ if (opts.dir) return path20.join(expandHome3(opts.dir), ".openacp");
3573
+ if (opts.local) return path20.join(cwd, ".openacp");
3574
+ const cwdRoot = path20.join(cwd, ".openacp");
3575
+ if (fs21.existsSync(path20.join(cwdRoot, "config.json"))) return cwdRoot;
3576
+ let dir = path20.resolve(cwd);
3577
+ while (true) {
3578
+ const parent = path20.dirname(dir);
3579
+ if (parent === dir) break;
3580
+ dir = parent;
3581
+ const candidate = path20.join(dir, ".openacp");
3582
+ if (candidate === globalRoot) {
3583
+ if (dir === home) break;
3584
+ continue;
3585
+ }
3586
+ if (fs21.existsSync(path20.join(candidate, "config.json"))) return candidate;
3587
+ if (dir === home) break;
3588
+ }
3589
+ if (path20.resolve(cwd) === path20.resolve(home)) {
3590
+ const defaultWs = path20.join(home, "openacp-workspace", ".openacp");
3591
+ if (fs21.existsSync(path20.join(defaultWs, "config.json"))) return defaultWs;
3592
+ }
3593
+ if (process.env.OPENACP_INSTANCE_ROOT) return process.env.OPENACP_INSTANCE_ROOT;
3594
+ return null;
3595
+ }
3596
+ function getGlobalRoot() {
3597
+ return path20.join(os6.homedir(), ".openacp");
3598
+ }
3599
+ async function resolveRunningInstance(cwd) {
3600
+ const globalRoot = getGlobalRoot();
3601
+ const home = os6.homedir();
3602
+ let dir = path20.resolve(cwd);
3603
+ while (true) {
3604
+ const candidate = path20.join(dir, ".openacp");
3605
+ if (candidate !== globalRoot && fs21.existsSync(candidate)) {
3606
+ if (await isInstanceRunning(candidate)) return candidate;
3607
+ }
3608
+ const parent = path20.dirname(dir);
3609
+ if (parent === dir) break;
3610
+ if (dir === home) break;
3611
+ dir = parent;
3612
+ }
3613
+ return null;
3614
+ }
3615
+ async function isInstanceRunning(instanceRoot) {
3616
+ const portFile = path20.join(instanceRoot, "api.port");
3617
+ try {
3618
+ const content = fs21.readFileSync(portFile, "utf-8").trim();
3619
+ const port = parseInt(content, 10);
3620
+ if (isNaN(port)) return false;
3621
+ const controller = new AbortController();
3622
+ const timeout = setTimeout(() => controller.abort(), 2e3);
3623
+ const res = await fetch(`http://127.0.0.1:${port}/api/v1/system/health`, {
3624
+ signal: controller.signal
3625
+ });
3626
+ clearTimeout(timeout);
3627
+ return res.ok;
3628
+ } catch {
3629
+ return false;
3630
+ }
3631
+ }
3632
+ var init_instance_context = __esm({
3633
+ "src/core/instance/instance-context.ts"() {
3634
+ "use strict";
3635
+ }
3636
+ });
3637
+
3153
3638
  // src/core/doctor/checks/telegram.ts
3154
- import * as path19 from "path";
3639
+ import * as path21 from "path";
3155
3640
  var BOT_TOKEN_REGEX, telegramCheck;
3156
3641
  var init_telegram = __esm({
3157
3642
  "src/core/doctor/checks/telegram.ts"() {
3158
3643
  "use strict";
3159
3644
  init_network_redaction();
3645
+ init_telegram_command_scopes();
3160
3646
  BOT_TOKEN_REGEX = /^\d+:[A-Za-z0-9_-]{35,}$/;
3161
3647
  telegramCheck = {
3162
3648
  name: "Telegram",
@@ -3168,7 +3654,7 @@ var init_telegram = __esm({
3168
3654
  return results;
3169
3655
  }
3170
3656
  const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
3171
- const sm = new SettingsManager2(path19.join(ctx.pluginsDir, "data"));
3657
+ const sm = new SettingsManager2(path21.join(ctx.pluginsDir, "data"));
3172
3658
  const ps = await sm.loadSettings("@openacp/telegram");
3173
3659
  const botToken = ps.botToken;
3174
3660
  const chatId = ps.chatId;
@@ -3209,6 +3695,24 @@ var init_telegram = __esm({
3209
3695
  results.push({ status: "fail", message: "Chat ID not configured" });
3210
3696
  return results;
3211
3697
  }
3698
+ try {
3699
+ const [{ TelegramCommandOwnershipStore: TelegramCommandOwnershipStore2, telegramCommandInstanceKey: telegramCommandInstanceKey2 }, { getGlobalRoot: getGlobalRoot2 }] = await Promise.all([
3700
+ Promise.resolve().then(() => (init_command_ownership_store(), command_ownership_store_exports)),
3701
+ Promise.resolve().then(() => (init_instance_context(), instance_context_exports))
3702
+ ]);
3703
+ const owner = new TelegramCommandOwnershipStore2(getGlobalRoot2()).getOwner(String(botId));
3704
+ if (owner && owner.instanceKey !== telegramCommandInstanceKey2(ctx.dataDir)) {
3705
+ results.push({
3706
+ status: "warn",
3707
+ message: "Telegram command sync is owned by another OpenACP instance. Use a unique bot per instance. For a stopped same-host owner only, request one explicit takeover with OPENACP_TELEGRAM_COMMAND_TAKEOVER=1."
3708
+ });
3709
+ }
3710
+ } catch (err) {
3711
+ results.push({
3712
+ status: "warn",
3713
+ message: `Cannot inspect Telegram command-sync ownership: ${redactNetworkSecrets(err instanceof Error ? err.message : String(err))}`
3714
+ });
3715
+ }
3212
3716
  try {
3213
3717
  const res = await telegramFetch(`https://api.telegram.org/bot${botToken}/getChat`, {
3214
3718
  method: "POST",
@@ -3255,6 +3759,54 @@ var init_telegram = __esm({
3255
3759
  const message = err instanceof Error ? err.message : String(err);
3256
3760
  results.push({ status: "fail", message: `Admin check failed: ${redactNetworkSecrets(message)}` });
3257
3761
  }
3762
+ try {
3763
+ const getCommands = async (scope, languageCode) => {
3764
+ const res = await telegramFetch(`https://api.telegram.org/bot${botToken}/getMyCommands`, {
3765
+ method: "POST",
3766
+ headers: { "Content-Type": "application/json" },
3767
+ body: JSON.stringify({ scope, ...languageCode ? { language_code: languageCode } : {} })
3768
+ });
3769
+ const data = await res.json();
3770
+ if (!data.ok || !Array.isArray(data.result)) {
3771
+ throw new Error(data.description || "Telegram returned an invalid command list");
3772
+ }
3773
+ return data.result;
3774
+ };
3775
+ const missing = [];
3776
+ for (const locale of TELEGRAM_COMMAND_LOCALES) {
3777
+ const [defaultCommands, groupCommands, globalAdminCommands, chatCommands, chatAdminCommands] = await Promise.all([
3778
+ getCommands({ type: "default" }, locale),
3779
+ getCommands({ type: "all_group_chats" }, locale),
3780
+ getCommands({ type: "all_chat_administrators" }, locale),
3781
+ getCommands({ type: "chat", chat_id: chatId }, locale),
3782
+ getCommands({ type: "chat_administrators", chat_id: chatId }, locale)
3783
+ ]);
3784
+ const lists = {
3785
+ default: defaultCommands,
3786
+ allGroup: groupCommands,
3787
+ allAdmins: globalAdminCommands,
3788
+ chat: chatCommands,
3789
+ chatAdmins: chatAdminCommands
3790
+ };
3791
+ const localeName = locale || "neutral";
3792
+ if (!effectiveTelegramGroupCommands(lists, false).some((command) => command.command === "proxy")) missing.push(`${localeName} members`);
3793
+ if (!effectiveTelegramGroupCommands(lists, true).some((command) => command.command === "proxy")) missing.push(`${localeName} administrators`);
3794
+ }
3795
+ if (missing.length === 0) {
3796
+ results.push({ status: "pass", message: "Telegram command menus are synchronized for neutral/en/ru scopes (including /proxy)" });
3797
+ } else {
3798
+ results.push({
3799
+ status: "warn",
3800
+ message: `Telegram command menu is out of sync for ${missing.join(", ")} (missing /proxy). Restart OpenACP; if the warning remains, inspect Telegram connectivity logs.`
3801
+ });
3802
+ }
3803
+ } catch (err) {
3804
+ const message = redactNetworkSecrets(err instanceof Error ? err.message : String(err));
3805
+ results.push({
3806
+ status: "warn",
3807
+ message: `Cannot verify Telegram command menus: ${message}. Restart OpenACP; if the warning remains, inspect Telegram connectivity logs.`
3808
+ });
3809
+ }
3258
3810
  return results;
3259
3811
  }
3260
3812
  };
@@ -3262,7 +3814,7 @@ var init_telegram = __esm({
3262
3814
  });
3263
3815
 
3264
3816
  // src/core/doctor/checks/storage.ts
3265
- import * as fs20 from "fs";
3817
+ import * as fs22 from "fs";
3266
3818
  var storageCheck;
3267
3819
  var init_storage = __esm({
3268
3820
  "src/core/doctor/checks/storage.ts"() {
@@ -3272,28 +3824,28 @@ var init_storage = __esm({
3272
3824
  order: 4,
3273
3825
  async run(ctx) {
3274
3826
  const results = [];
3275
- if (!fs20.existsSync(ctx.dataDir)) {
3827
+ if (!fs22.existsSync(ctx.dataDir)) {
3276
3828
  results.push({
3277
3829
  status: "fail",
3278
3830
  message: "Data directory ~/.openacp does not exist",
3279
3831
  fixable: true,
3280
3832
  fixRisk: "safe",
3281
3833
  fix: async () => {
3282
- fs20.mkdirSync(ctx.dataDir, { recursive: true });
3834
+ fs22.mkdirSync(ctx.dataDir, { recursive: true });
3283
3835
  return { success: true, message: "created directory" };
3284
3836
  }
3285
3837
  });
3286
3838
  } else {
3287
3839
  try {
3288
- fs20.accessSync(ctx.dataDir, fs20.constants.W_OK);
3840
+ fs22.accessSync(ctx.dataDir, fs22.constants.W_OK);
3289
3841
  results.push({ status: "pass", message: "Data directory exists and writable" });
3290
3842
  } catch {
3291
3843
  results.push({ status: "fail", message: "Data directory not writable" });
3292
3844
  }
3293
3845
  }
3294
- if (fs20.existsSync(ctx.sessionsPath)) {
3846
+ if (fs22.existsSync(ctx.sessionsPath)) {
3295
3847
  try {
3296
- const content = fs20.readFileSync(ctx.sessionsPath, "utf-8");
3848
+ const content = fs22.readFileSync(ctx.sessionsPath, "utf-8");
3297
3849
  const data = JSON.parse(content);
3298
3850
  if (typeof data === "object" && data !== null && "sessions" in data) {
3299
3851
  results.push({ status: "pass", message: "Sessions file valid" });
@@ -3304,7 +3856,7 @@ var init_storage = __esm({
3304
3856
  fixable: true,
3305
3857
  fixRisk: "risky",
3306
3858
  fix: async () => {
3307
- fs20.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
3859
+ fs22.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
3308
3860
  return { success: true, message: "reset sessions file" };
3309
3861
  }
3310
3862
  });
@@ -3316,7 +3868,7 @@ var init_storage = __esm({
3316
3868
  fixable: true,
3317
3869
  fixRisk: "risky",
3318
3870
  fix: async () => {
3319
- fs20.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
3871
+ fs22.writeFileSync(ctx.sessionsPath, JSON.stringify({ version: 1, sessions: {} }, null, 2));
3320
3872
  return { success: true, message: "reset sessions file" };
3321
3873
  }
3322
3874
  });
@@ -3324,20 +3876,20 @@ var init_storage = __esm({
3324
3876
  } else {
3325
3877
  results.push({ status: "pass", message: "Sessions file not present yet (created on first session)" });
3326
3878
  }
3327
- if (!fs20.existsSync(ctx.logsDir)) {
3879
+ if (!fs22.existsSync(ctx.logsDir)) {
3328
3880
  results.push({
3329
3881
  status: "warn",
3330
3882
  message: "Log directory does not exist",
3331
3883
  fixable: true,
3332
3884
  fixRisk: "safe",
3333
3885
  fix: async () => {
3334
- fs20.mkdirSync(ctx.logsDir, { recursive: true });
3886
+ fs22.mkdirSync(ctx.logsDir, { recursive: true });
3335
3887
  return { success: true, message: "created log directory" };
3336
3888
  }
3337
3889
  });
3338
3890
  } else {
3339
3891
  try {
3340
- fs20.accessSync(ctx.logsDir, fs20.constants.W_OK);
3892
+ fs22.accessSync(ctx.logsDir, fs22.constants.W_OK);
3341
3893
  results.push({ status: "pass", message: "Log directory exists and writable" });
3342
3894
  } catch {
3343
3895
  results.push({ status: "fail", message: "Log directory not writable" });
@@ -3350,8 +3902,8 @@ var init_storage = __esm({
3350
3902
  });
3351
3903
 
3352
3904
  // src/core/doctor/checks/workspace.ts
3353
- import * as fs21 from "fs";
3354
- import * as path20 from "path";
3905
+ import * as fs23 from "fs";
3906
+ import * as path22 from "path";
3355
3907
  var workspaceCheck;
3356
3908
  var init_workspace = __esm({
3357
3909
  "src/core/doctor/checks/workspace.ts"() {
@@ -3361,21 +3913,21 @@ var init_workspace = __esm({
3361
3913
  order: 5,
3362
3914
  async run(ctx) {
3363
3915
  const results = [];
3364
- const workspace = path20.dirname(ctx.dataDir);
3365
- if (!fs21.existsSync(workspace)) {
3916
+ const workspace = path22.dirname(ctx.dataDir);
3917
+ if (!fs23.existsSync(workspace)) {
3366
3918
  results.push({
3367
3919
  status: "warn",
3368
3920
  message: `Workspace directory does not exist: ${workspace}`,
3369
3921
  fixable: true,
3370
3922
  fixRisk: "safe",
3371
3923
  fix: async () => {
3372
- fs21.mkdirSync(workspace, { recursive: true });
3924
+ fs23.mkdirSync(workspace, { recursive: true });
3373
3925
  return { success: true, message: "created directory" };
3374
3926
  }
3375
3927
  });
3376
3928
  } else {
3377
3929
  try {
3378
- fs21.accessSync(workspace, fs21.constants.W_OK);
3930
+ fs23.accessSync(workspace, fs23.constants.W_OK);
3379
3931
  results.push({ status: "pass", message: `Workspace directory exists: ${workspace}` });
3380
3932
  } catch {
3381
3933
  results.push({ status: "fail", message: `Workspace directory not writable: ${workspace}` });
@@ -3388,8 +3940,8 @@ var init_workspace = __esm({
3388
3940
  });
3389
3941
 
3390
3942
  // src/core/doctor/checks/plugins.ts
3391
- import * as fs22 from "fs";
3392
- import * as path21 from "path";
3943
+ import * as fs24 from "fs";
3944
+ import * as path23 from "path";
3393
3945
  var pluginsCheck;
3394
3946
  var init_plugins = __esm({
3395
3947
  "src/core/doctor/checks/plugins.ts"() {
@@ -3399,16 +3951,16 @@ var init_plugins = __esm({
3399
3951
  order: 6,
3400
3952
  async run(ctx) {
3401
3953
  const results = [];
3402
- if (!fs22.existsSync(ctx.pluginsDir)) {
3954
+ if (!fs24.existsSync(ctx.pluginsDir)) {
3403
3955
  results.push({
3404
3956
  status: "warn",
3405
3957
  message: "Plugins directory does not exist",
3406
3958
  fixable: true,
3407
3959
  fixRisk: "safe",
3408
3960
  fix: async () => {
3409
- fs22.mkdirSync(ctx.pluginsDir, { recursive: true });
3410
- fs22.writeFileSync(
3411
- path21.join(ctx.pluginsDir, "package.json"),
3961
+ fs24.mkdirSync(ctx.pluginsDir, { recursive: true });
3962
+ fs24.writeFileSync(
3963
+ path23.join(ctx.pluginsDir, "package.json"),
3412
3964
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
3413
3965
  );
3414
3966
  return { success: true, message: "initialized plugins directory" };
@@ -3417,15 +3969,15 @@ var init_plugins = __esm({
3417
3969
  return results;
3418
3970
  }
3419
3971
  results.push({ status: "pass", message: "Plugins directory exists" });
3420
- const pkgPath = path21.join(ctx.pluginsDir, "package.json");
3421
- if (!fs22.existsSync(pkgPath)) {
3972
+ const pkgPath = path23.join(ctx.pluginsDir, "package.json");
3973
+ if (!fs24.existsSync(pkgPath)) {
3422
3974
  results.push({
3423
3975
  status: "warn",
3424
3976
  message: "Plugins package.json missing",
3425
3977
  fixable: true,
3426
3978
  fixRisk: "safe",
3427
3979
  fix: async () => {
3428
- fs22.writeFileSync(
3980
+ fs24.writeFileSync(
3429
3981
  pkgPath,
3430
3982
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
3431
3983
  );
@@ -3435,7 +3987,7 @@ var init_plugins = __esm({
3435
3987
  return results;
3436
3988
  }
3437
3989
  try {
3438
- const pkg = JSON.parse(fs22.readFileSync(pkgPath, "utf-8"));
3990
+ const pkg = JSON.parse(fs24.readFileSync(pkgPath, "utf-8"));
3439
3991
  const deps = pkg.dependencies || {};
3440
3992
  const count = Object.keys(deps).length;
3441
3993
  results.push({ status: "pass", message: `Plugins package.json valid (${count} plugins)` });
@@ -3446,7 +3998,7 @@ var init_plugins = __esm({
3446
3998
  fixable: true,
3447
3999
  fixRisk: "risky",
3448
4000
  fix: async () => {
3449
- fs22.writeFileSync(
4001
+ fs24.writeFileSync(
3450
4002
  pkgPath,
3451
4003
  JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2)
3452
4004
  );
@@ -3461,7 +4013,7 @@ var init_plugins = __esm({
3461
4013
  });
3462
4014
 
3463
4015
  // src/core/doctor/checks/daemon.ts
3464
- import * as fs23 from "fs";
4016
+ import * as fs25 from "fs";
3465
4017
  import * as net3 from "net";
3466
4018
  function isProcessAlive(pid) {
3467
4019
  try {
@@ -3491,8 +4043,8 @@ var init_daemon = __esm({
3491
4043
  order: 7,
3492
4044
  async run(ctx) {
3493
4045
  const results = [];
3494
- if (fs23.existsSync(ctx.pidPath)) {
3495
- const content = fs23.readFileSync(ctx.pidPath, "utf-8").trim();
4046
+ if (fs25.existsSync(ctx.pidPath)) {
4047
+ const content = fs25.readFileSync(ctx.pidPath, "utf-8").trim();
3496
4048
  const pid = parseInt(content, 10);
3497
4049
  if (isNaN(pid)) {
3498
4050
  results.push({
@@ -3501,7 +4053,7 @@ var init_daemon = __esm({
3501
4053
  fixable: true,
3502
4054
  fixRisk: "safe",
3503
4055
  fix: async () => {
3504
- fs23.unlinkSync(ctx.pidPath);
4056
+ fs25.unlinkSync(ctx.pidPath);
3505
4057
  return { success: true, message: "removed invalid PID file" };
3506
4058
  }
3507
4059
  });
@@ -3512,7 +4064,7 @@ var init_daemon = __esm({
3512
4064
  fixable: true,
3513
4065
  fixRisk: "safe",
3514
4066
  fix: async () => {
3515
- fs23.unlinkSync(ctx.pidPath);
4067
+ fs25.unlinkSync(ctx.pidPath);
3516
4068
  return { success: true, message: "removed stale PID file" };
3517
4069
  }
3518
4070
  });
@@ -3520,8 +4072,8 @@ var init_daemon = __esm({
3520
4072
  results.push({ status: "pass", message: `Daemon running (PID ${pid})` });
3521
4073
  }
3522
4074
  }
3523
- if (fs23.existsSync(ctx.portFilePath)) {
3524
- const content = fs23.readFileSync(ctx.portFilePath, "utf-8").trim();
4075
+ if (fs25.existsSync(ctx.portFilePath)) {
4076
+ const content = fs25.readFileSync(ctx.portFilePath, "utf-8").trim();
3525
4077
  const port = parseInt(content, 10);
3526
4078
  if (isNaN(port)) {
3527
4079
  results.push({
@@ -3530,7 +4082,7 @@ var init_daemon = __esm({
3530
4082
  fixable: true,
3531
4083
  fixRisk: "safe",
3532
4084
  fix: async () => {
3533
- fs23.unlinkSync(ctx.portFilePath);
4085
+ fs25.unlinkSync(ctx.portFilePath);
3534
4086
  return { success: true, message: "removed invalid port file" };
3535
4087
  }
3536
4088
  });
@@ -3542,8 +4094,8 @@ var init_daemon = __esm({
3542
4094
  const apiPort = 21420;
3543
4095
  const inUse = await checkPortInUse(apiPort);
3544
4096
  if (inUse) {
3545
- if (fs23.existsSync(ctx.pidPath)) {
3546
- const pid = parseInt(fs23.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
4097
+ if (fs25.existsSync(ctx.pidPath)) {
4098
+ const pid = parseInt(fs25.readFileSync(ctx.pidPath, "utf-8").trim(), 10);
3547
4099
  if (!isNaN(pid) && isProcessAlive(pid)) {
3548
4100
  results.push({ status: "pass", message: `API port ${apiPort} in use by OpenACP daemon` });
3549
4101
  } else {
@@ -3563,17 +4115,17 @@ var init_daemon = __esm({
3563
4115
  });
3564
4116
 
3565
4117
  // src/core/utils/install-binary.ts
3566
- import fs24 from "fs";
3567
- import path22 from "path";
4118
+ import fs26 from "fs";
4119
+ import path24 from "path";
3568
4120
  import https from "https";
3569
- import os5 from "os";
4121
+ import os7 from "os";
3570
4122
  import { execFileSync as execFileSync4 } from "child_process";
3571
4123
  function downloadFile(url, dest, maxRedirects = 10) {
3572
4124
  return new Promise((resolve7, reject) => {
3573
- const file = fs24.createWriteStream(dest);
4125
+ const file = fs26.createWriteStream(dest);
3574
4126
  const cleanup = () => {
3575
4127
  try {
3576
- if (fs24.existsSync(dest)) fs24.unlinkSync(dest);
4128
+ if (fs26.existsSync(dest)) fs26.unlinkSync(dest);
3577
4129
  } catch {
3578
4130
  }
3579
4131
  };
@@ -3628,8 +4180,8 @@ function downloadFile(url, dest, maxRedirects = 10) {
3628
4180
  });
3629
4181
  }
3630
4182
  function getDownloadUrl(spec) {
3631
- const platform2 = os5.platform();
3632
- const arch = os5.arch();
4183
+ const platform2 = os7.platform();
4184
+ const arch = os7.arch();
3633
4185
  const mapping = spec.platforms[platform2];
3634
4186
  if (!mapping) throw new Error(`${spec.name}: unsupported platform ${platform2}`);
3635
4187
  const binary = mapping[arch];
@@ -3639,50 +4191,50 @@ function getDownloadUrl(spec) {
3639
4191
  async function ensureBinary(spec, binDir) {
3640
4192
  const resolvedBinDir = binDir ?? DEFAULT_BIN_DIR;
3641
4193
  const binName = IS_WINDOWS ? `${spec.name}.exe` : spec.name;
3642
- const binPath = path22.join(resolvedBinDir, binName);
4194
+ const binPath = path24.join(resolvedBinDir, binName);
3643
4195
  if (commandExists(spec.name)) {
3644
- log18.debug({ name: spec.name }, "Found in PATH");
4196
+ log19.debug({ name: spec.name }, "Found in PATH");
3645
4197
  return spec.name;
3646
4198
  }
3647
- if (fs24.existsSync(binPath)) {
3648
- if (!IS_WINDOWS) fs24.chmodSync(binPath, "755");
3649
- log18.debug({ name: spec.name, path: binPath }, "Found in ~/.openacp/bin");
4199
+ if (fs26.existsSync(binPath)) {
4200
+ if (!IS_WINDOWS) fs26.chmodSync(binPath, "755");
4201
+ log19.debug({ name: spec.name, path: binPath }, "Found in ~/.openacp/bin");
3650
4202
  return binPath;
3651
4203
  }
3652
- log18.info({ name: spec.name }, "Not found, downloading from GitHub...");
3653
- fs24.mkdirSync(resolvedBinDir, { recursive: true });
4204
+ log19.info({ name: spec.name }, "Not found, downloading from GitHub...");
4205
+ fs26.mkdirSync(resolvedBinDir, { recursive: true });
3654
4206
  const url = getDownloadUrl(spec);
3655
4207
  const isArchive = spec.isArchive?.(url) ?? false;
3656
- const downloadDest = isArchive ? path22.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
4208
+ const downloadDest = isArchive ? path24.join(resolvedBinDir, `${spec.name}.tgz`) : binPath;
3657
4209
  await downloadFile(url, downloadDest);
3658
4210
  if (isArchive) {
3659
4211
  const listing = execFileSync4("tar", ["-tf", downloadDest], { stdio: "pipe" }).toString().trim().split("\n").filter(Boolean);
3660
4212
  validateTarContents(listing, resolvedBinDir);
3661
4213
  execFileSync4("tar", ["-xzf", downloadDest, "-C", resolvedBinDir], { stdio: "pipe" });
3662
4214
  try {
3663
- fs24.unlinkSync(downloadDest);
4215
+ fs26.unlinkSync(downloadDest);
3664
4216
  } catch {
3665
4217
  }
3666
4218
  }
3667
- if (!fs24.existsSync(binPath)) {
4219
+ if (!fs26.existsSync(binPath)) {
3668
4220
  throw new Error(`${spec.name}: binary not found at ${binPath} after download/extraction. The archive structure may have changed.`);
3669
4221
  }
3670
4222
  if (!IS_WINDOWS) {
3671
- fs24.chmodSync(binPath, "755");
4223
+ fs26.chmodSync(binPath, "755");
3672
4224
  }
3673
- log18.info({ name: spec.name, path: binPath }, "Installed successfully");
4225
+ log19.info({ name: spec.name, path: binPath }, "Installed successfully");
3674
4226
  return binPath;
3675
4227
  }
3676
- var log18, DEFAULT_BIN_DIR, IS_WINDOWS;
4228
+ var log19, DEFAULT_BIN_DIR, IS_WINDOWS;
3677
4229
  var init_install_binary = __esm({
3678
4230
  "src/core/utils/install-binary.ts"() {
3679
4231
  "use strict";
3680
4232
  init_log();
3681
4233
  init_agent_dependencies();
3682
4234
  init_agent_installer();
3683
- log18 = createChildLogger({ module: "binary-installer" });
3684
- DEFAULT_BIN_DIR = path22.join(os5.homedir(), ".openacp", "bin");
3685
- IS_WINDOWS = os5.platform() === "win32";
4235
+ log19 = createChildLogger({ module: "binary-installer" });
4236
+ DEFAULT_BIN_DIR = path24.join(os7.homedir(), ".openacp", "bin");
4237
+ IS_WINDOWS = os7.platform() === "win32";
3686
4238
  }
3687
4239
  });
3688
4240
 
@@ -3722,9 +4274,9 @@ var init_install_cloudflared = __esm({
3722
4274
  });
3723
4275
 
3724
4276
  // src/core/doctor/checks/tunnel.ts
3725
- import * as fs25 from "fs";
3726
- import * as path23 from "path";
3727
- import * as os6 from "os";
4277
+ import * as fs27 from "fs";
4278
+ import * as path25 from "path";
4279
+ import * as os8 from "os";
3728
4280
  import { execFileSync as execFileSync5 } from "child_process";
3729
4281
  var tunnelCheck;
3730
4282
  var init_tunnel = __esm({
@@ -3740,7 +4292,7 @@ var init_tunnel = __esm({
3740
4292
  return results;
3741
4293
  }
3742
4294
  const { SettingsManager: SettingsManager2 } = await Promise.resolve().then(() => (init_settings_manager(), settings_manager_exports));
3743
- const sm = new SettingsManager2(path23.join(ctx.pluginsDir, "data"));
4295
+ const sm = new SettingsManager2(path25.join(ctx.pluginsDir, "data"));
3744
4296
  const tunnelSettings = await sm.loadSettings("@openacp/tunnel");
3745
4297
  const tunnelEnabled = tunnelSettings.enabled ?? false;
3746
4298
  const provider = tunnelSettings.provider ?? "cloudflare";
@@ -3751,10 +4303,10 @@ var init_tunnel = __esm({
3751
4303
  }
3752
4304
  results.push({ status: "pass", message: `Tunnel provider: ${provider}` });
3753
4305
  if (provider === "cloudflare") {
3754
- const binName = os6.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
3755
- const binPath = path23.join(ctx.dataDir, "bin", binName);
4306
+ const binName = os8.platform() === "win32" ? "cloudflared.exe" : "cloudflared";
4307
+ const binPath = path25.join(ctx.dataDir, "bin", binName);
3756
4308
  let found = false;
3757
- if (fs25.existsSync(binPath)) {
4309
+ if (fs27.existsSync(binPath)) {
3758
4310
  found = true;
3759
4311
  } else {
3760
4312
  try {
@@ -3828,8 +4380,8 @@ var init_proxy = __esm({
3828
4380
  });
3829
4381
 
3830
4382
  // src/core/doctor/index.ts
3831
- import * as fs26 from "fs";
3832
- import * as path24 from "path";
4383
+ import * as fs28 from "fs";
4384
+ import * as path26 from "path";
3833
4385
  var ALL_CHECKS, CHECK_TIMEOUT_MS, DoctorEngine;
3834
4386
  var init_doctor = __esm({
3835
4387
  "src/core/doctor/index.ts"() {
@@ -3922,27 +4474,27 @@ var init_doctor = __esm({
3922
4474
  /** Constructs the shared context used by all checks — loads config if available. */
3923
4475
  async buildContext() {
3924
4476
  const dataDir = this.dataDir;
3925
- const configPath = process.env.OPENACP_CONFIG_PATH || path24.join(dataDir, "config.json");
4477
+ const configPath = process.env.OPENACP_CONFIG_PATH || path26.join(dataDir, "config.json");
3926
4478
  let config = null;
3927
4479
  let rawConfig = null;
3928
4480
  try {
3929
- const content = fs26.readFileSync(configPath, "utf-8");
4481
+ const content = fs28.readFileSync(configPath, "utf-8");
3930
4482
  rawConfig = JSON.parse(content);
3931
4483
  const cm = new ConfigManager(configPath);
3932
4484
  await cm.load();
3933
4485
  config = cm.get();
3934
4486
  } catch {
3935
4487
  }
3936
- const logsDir = config ? expandHome2(config.logging.logDir) : path24.join(dataDir, "logs");
4488
+ const logsDir = config ? expandHome2(config.logging.logDir) : path26.join(dataDir, "logs");
3937
4489
  return {
3938
4490
  config,
3939
4491
  rawConfig,
3940
4492
  configPath,
3941
4493
  dataDir,
3942
- sessionsPath: path24.join(dataDir, "sessions.json"),
3943
- pidPath: path24.join(dataDir, "openacp.pid"),
3944
- portFilePath: path24.join(dataDir, "api.port"),
3945
- pluginsDir: path24.join(dataDir, "plugins"),
4494
+ sessionsPath: path26.join(dataDir, "sessions.json"),
4495
+ pidPath: path26.join(dataDir, "openacp.pid"),
4496
+ portFilePath: path26.join(dataDir, "api.port"),
4497
+ pluginsDir: path26.join(dataDir, "plugins"),
3946
4498
  logsDir,
3947
4499
  fetchForScope: (scope) => this.proxyService.createFetch(scope)
3948
4500
  };
@@ -3951,26 +4503,13 @@ var init_doctor = __esm({
3951
4503
  }
3952
4504
  });
3953
4505
 
3954
- // src/core/instance/instance-context.ts
3955
- import path26 from "path";
3956
- import fs28 from "fs";
3957
- import os8 from "os";
3958
- function getGlobalRoot() {
3959
- return path26.join(os8.homedir(), ".openacp");
3960
- }
3961
- var init_instance_context = __esm({
3962
- "src/core/instance/instance-context.ts"() {
3963
- "use strict";
3964
- }
3965
- });
3966
-
3967
4506
  // src/core/instance/instance-registry.ts
3968
- import fs29 from "fs";
3969
- import path27 from "path";
4507
+ import fs30 from "fs";
4508
+ import path28 from "path";
3970
4509
  import { randomUUID as randomUUID2 } from "crypto";
3971
4510
  function readIdFromConfig(instanceRoot) {
3972
4511
  try {
3973
- const raw = JSON.parse(fs29.readFileSync(path27.join(instanceRoot, "config.json"), "utf-8"));
4512
+ const raw = JSON.parse(fs30.readFileSync(path28.join(instanceRoot, "config.json"), "utf-8"));
3974
4513
  return typeof raw.id === "string" && raw.id ? raw.id : null;
3975
4514
  } catch {
3976
4515
  return null;
@@ -3989,7 +4528,7 @@ var init_instance_registry = __esm({
3989
4528
  /** Load the registry from disk. If the file is missing or corrupt, starts fresh. */
3990
4529
  load() {
3991
4530
  try {
3992
- const raw = fs29.readFileSync(this.registryPath, "utf-8");
4531
+ const raw = fs30.readFileSync(this.registryPath, "utf-8");
3993
4532
  const parsed = JSON.parse(raw);
3994
4533
  if (parsed.version === 1 && parsed.instances) {
3995
4534
  this.data = parsed;
@@ -4016,9 +4555,9 @@ var init_instance_registry = __esm({
4016
4555
  }
4017
4556
  /** Persist the registry to disk, creating parent directories if needed. */
4018
4557
  save() {
4019
- const dir = path27.dirname(this.registryPath);
4020
- fs29.mkdirSync(dir, { recursive: true });
4021
- fs29.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
4558
+ const dir = path28.dirname(this.registryPath);
4559
+ fs30.mkdirSync(dir, { recursive: true });
4560
+ fs30.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
4022
4561
  }
4023
4562
  /** Add or update an instance entry in the registry. Does not persist — call save() after. */
4024
4563
  register(id, root) {
@@ -4085,15 +4624,15 @@ __export(plugin_installer_exports, {
4085
4624
  });
4086
4625
  import { execFile as execFile2 } from "child_process";
4087
4626
  import { promisify as promisify2 } from "util";
4088
- import * as fs31 from "fs/promises";
4089
- import * as path29 from "path";
4627
+ import * as fs32 from "fs/promises";
4628
+ import * as path30 from "path";
4090
4629
  import { pathToFileURL } from "url";
4091
4630
  async function importFromDir(packageName, dir) {
4092
- const pkgDir = path29.join(dir, "node_modules", ...packageName.split("/"));
4093
- const pkgJsonPath = path29.join(pkgDir, "package.json");
4631
+ const pkgDir = path30.join(dir, "node_modules", ...packageName.split("/"));
4632
+ const pkgJsonPath = path30.join(pkgDir, "package.json");
4094
4633
  let pkgJson;
4095
4634
  try {
4096
- pkgJson = JSON.parse(await fs31.readFile(pkgJsonPath, "utf-8"));
4635
+ pkgJson = JSON.parse(await fs32.readFile(pkgJsonPath, "utf-8"));
4097
4636
  } catch (err) {
4098
4637
  throw new Error(`Cannot read package.json for "${packageName}" at ${pkgJsonPath}: ${err.message}`);
4099
4638
  }
@@ -4106,9 +4645,9 @@ async function importFromDir(packageName, dir) {
4106
4645
  } else {
4107
4646
  entry = pkgJson.main ?? "index.js";
4108
4647
  }
4109
- const entryPath = path29.join(pkgDir, entry);
4648
+ const entryPath = path30.join(pkgDir, entry);
4110
4649
  try {
4111
- await fs31.access(entryPath);
4650
+ await fs32.access(entryPath);
4112
4651
  } catch {
4113
4652
  throw new Error(`Entry point "${entry}" not found for "${packageName}" at ${entryPath}`);
4114
4653
  }
@@ -4201,10 +4740,10 @@ var install_context_exports = {};
4201
4740
  __export(install_context_exports, {
4202
4741
  createInstallContext: () => createInstallContext
4203
4742
  });
4204
- import path30 from "path";
4743
+ import path31 from "path";
4205
4744
  function createInstallContext(opts) {
4206
4745
  const { pluginName, settingsManager, basePath, instanceRoot } = opts;
4207
- const dataDir = path30.join(basePath, pluginName, "data");
4746
+ const dataDir = path31.join(basePath, pluginName, "data");
4208
4747
  return {
4209
4748
  pluginName,
4210
4749
  terminal: createTerminalIO(),
@@ -4232,14 +4771,14 @@ __export(api_client_exports, {
4232
4771
  waitForApiReady: () => waitForApiReady,
4233
4772
  waitForPortFile: () => waitForPortFile
4234
4773
  });
4235
- import * as fs32 from "fs";
4236
- import * as path31 from "path";
4774
+ import * as fs33 from "fs";
4775
+ import * as path32 from "path";
4237
4776
  import { setTimeout as sleep } from "timers/promises";
4238
4777
  function defaultPortFile(root) {
4239
- return path31.join(root, "api.port");
4778
+ return path32.join(root, "api.port");
4240
4779
  }
4241
4780
  function defaultSecretFile(root) {
4242
- return path31.join(root, "api-secret");
4781
+ return path32.join(root, "api-secret");
4243
4782
  }
4244
4783
  async function waitForPortFile(portFilePath, timeoutMs = 5e3, intervalMs = 100) {
4245
4784
  const deadline = Date.now() + timeoutMs;
@@ -4273,7 +4812,7 @@ async function waitForApiReady(instanceRoot, expectedInstanceId, timeoutMs = 1e4
4273
4812
  function readApiPort(portFilePath, instanceRoot) {
4274
4813
  const filePath = portFilePath ?? defaultPortFile(instanceRoot);
4275
4814
  try {
4276
- const content = fs32.readFileSync(filePath, "utf-8").trim();
4815
+ const content = fs33.readFileSync(filePath, "utf-8").trim();
4277
4816
  const port = parseInt(content, 10);
4278
4817
  return isNaN(port) ? null : port;
4279
4818
  } catch {
@@ -4283,7 +4822,7 @@ function readApiPort(portFilePath, instanceRoot) {
4283
4822
  function readApiSecret(secretFilePath, instanceRoot) {
4284
4823
  const filePath = secretFilePath ?? defaultSecretFile(instanceRoot);
4285
4824
  try {
4286
- const content = fs32.readFileSync(filePath, "utf-8").trim();
4825
+ const content = fs33.readFileSync(filePath, "utf-8").trim();
4287
4826
  return content || null;
4288
4827
  } catch {
4289
4828
  return null;
@@ -4292,7 +4831,7 @@ function readApiSecret(secretFilePath, instanceRoot) {
4292
4831
  function removeStalePortFile(portFilePath, instanceRoot) {
4293
4832
  const filePath = portFilePath ?? defaultPortFile(instanceRoot);
4294
4833
  try {
4295
- fs32.unlinkSync(filePath);
4834
+ fs33.unlinkSync(filePath);
4296
4835
  } catch {
4297
4836
  }
4298
4837
  }
@@ -4429,8 +4968,8 @@ var init_notification = __esm({
4429
4968
  });
4430
4969
 
4431
4970
  // src/plugins/file-service/file-service.ts
4432
- import fs34 from "fs";
4433
- import path34 from "path";
4971
+ import fs35 from "fs";
4972
+ import path35 from "path";
4434
4973
  import { OggOpusDecoder } from "ogg-opus-decoder";
4435
4974
  import wav from "node-wav";
4436
4975
  function classifyMime(mimeType) {
@@ -4487,14 +5026,14 @@ var init_file_service = __esm({
4487
5026
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
4488
5027
  let removed = 0;
4489
5028
  try {
4490
- const entries = await fs34.promises.readdir(this.baseDir, { withFileTypes: true });
5029
+ const entries = await fs35.promises.readdir(this.baseDir, { withFileTypes: true });
4491
5030
  for (const entry of entries) {
4492
5031
  if (!entry.isDirectory()) continue;
4493
- const dirPath = path34.join(this.baseDir, entry.name);
5032
+ const dirPath = path35.join(this.baseDir, entry.name);
4494
5033
  try {
4495
- const stat = await fs34.promises.stat(dirPath);
5034
+ const stat = await fs35.promises.stat(dirPath);
4496
5035
  if (stat.mtimeMs < cutoff) {
4497
- await fs34.promises.rm(dirPath, { recursive: true, force: true });
5036
+ await fs35.promises.rm(dirPath, { recursive: true, force: true });
4498
5037
  removed++;
4499
5038
  }
4500
5039
  } catch {
@@ -4513,11 +5052,11 @@ var init_file_service = __esm({
4513
5052
  * so the user-facing name is not lost.
4514
5053
  */
4515
5054
  async saveFile(sessionId, fileName, data, mimeType) {
4516
- const sessionDir = path34.join(this.baseDir, sessionId);
4517
- await fs34.promises.mkdir(sessionDir, { recursive: true });
5055
+ const sessionDir = path35.join(this.baseDir, sessionId);
5056
+ await fs35.promises.mkdir(sessionDir, { recursive: true });
4518
5057
  const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
4519
- const filePath = path34.join(sessionDir, safeName);
4520
- await fs34.promises.writeFile(filePath, data);
5058
+ const filePath = path35.join(sessionDir, safeName);
5059
+ await fs35.promises.writeFile(filePath, data);
4521
5060
  return {
4522
5061
  type: classifyMime(mimeType),
4523
5062
  filePath,
@@ -4532,14 +5071,14 @@ var init_file_service = __esm({
4532
5071
  */
4533
5072
  async resolveFile(filePath) {
4534
5073
  try {
4535
- const stat = await fs34.promises.stat(filePath);
5074
+ const stat = await fs35.promises.stat(filePath);
4536
5075
  if (!stat.isFile()) return null;
4537
- const ext = path34.extname(filePath).toLowerCase();
5076
+ const ext = path35.extname(filePath).toLowerCase();
4538
5077
  const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
4539
5078
  return {
4540
5079
  type: classifyMime(mimeType),
4541
5080
  filePath,
4542
- fileName: path34.basename(filePath),
5081
+ fileName: path35.basename(filePath),
4543
5082
  mimeType,
4544
5083
  size: stat.size
4545
5084
  };
@@ -4804,7 +5343,7 @@ function createAuthPreHandler(getSecret, getJwtSecret, tokenStore) {
4804
5343
  const queryToken = request.query?.token;
4805
5344
  const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : queryToken;
4806
5345
  if (queryToken && !authHeader) {
4807
- log22.warn(
5346
+ log23.warn(
4808
5347
  { url: request.url.replace(/([?&]token=)[^&]+/, "$1[REDACTED]") },
4809
5348
  "Token passed via URL query param \u2014 use Authorization: Bearer header to avoid token leakage in tunnel/proxy logs"
4810
5349
  );
@@ -4858,7 +5397,7 @@ function requireRole(role) {
4858
5397
  }
4859
5398
  };
4860
5399
  }
4861
- var log22;
5400
+ var log23;
4862
5401
  var init_auth = __esm({
4863
5402
  "src/plugins/api-server/middleware/auth.ts"() {
4864
5403
  "use strict";
@@ -4866,7 +5405,7 @@ var init_auth = __esm({
4866
5405
  init_jwt();
4867
5406
  init_roles();
4868
5407
  init_log();
4869
- log22 = createChildLogger({ module: "api-auth" });
5408
+ log23 = createChildLogger({ module: "api-auth" });
4870
5409
  }
4871
5410
  });
4872
5411
 
@@ -5182,8 +5721,8 @@ data: ${JSON.stringify(data)}
5182
5721
  });
5183
5722
 
5184
5723
  // src/plugins/api-server/static-server.ts
5185
- import * as fs35 from "fs";
5186
- import * as path35 from "path";
5724
+ import * as fs36 from "fs";
5725
+ import * as path36 from "path";
5187
5726
  import { fileURLToPath as fileURLToPath2 } from "url";
5188
5727
  var MIME_TYPES, StaticServer;
5189
5728
  var init_static_server = __esm({
@@ -5207,16 +5746,16 @@ var init_static_server = __esm({
5207
5746
  this.uiDir = uiDir;
5208
5747
  if (!this.uiDir) {
5209
5748
  const __filename = fileURLToPath2(import.meta.url);
5210
- const candidate = path35.resolve(path35.dirname(__filename), "../../ui/dist");
5211
- if (fs35.existsSync(path35.join(candidate, "index.html"))) {
5749
+ const candidate = path36.resolve(path36.dirname(__filename), "../../ui/dist");
5750
+ if (fs36.existsSync(path36.join(candidate, "index.html"))) {
5212
5751
  this.uiDir = candidate;
5213
5752
  }
5214
5753
  if (!this.uiDir) {
5215
- const publishCandidate = path35.resolve(
5216
- path35.dirname(__filename),
5754
+ const publishCandidate = path36.resolve(
5755
+ path36.dirname(__filename),
5217
5756
  "../ui"
5218
5757
  );
5219
- if (fs35.existsSync(path35.join(publishCandidate, "index.html"))) {
5758
+ if (fs36.existsSync(path36.join(publishCandidate, "index.html"))) {
5220
5759
  this.uiDir = publishCandidate;
5221
5760
  }
5222
5761
  }
@@ -5234,23 +5773,23 @@ var init_static_server = __esm({
5234
5773
  serve(req, res) {
5235
5774
  if (!this.uiDir) return false;
5236
5775
  const urlPath = (req.url || "/").split("?")[0];
5237
- const safePath = path35.normalize(urlPath);
5238
- const filePath = path35.join(this.uiDir, safePath);
5239
- if (!filePath.startsWith(this.uiDir + path35.sep) && filePath !== this.uiDir)
5776
+ const safePath = path36.normalize(urlPath);
5777
+ const filePath = path36.join(this.uiDir, safePath);
5778
+ if (!filePath.startsWith(this.uiDir + path36.sep) && filePath !== this.uiDir)
5240
5779
  return false;
5241
5780
  let realFilePath;
5242
5781
  try {
5243
- realFilePath = fs35.realpathSync(filePath);
5782
+ realFilePath = fs36.realpathSync(filePath);
5244
5783
  } catch {
5245
5784
  realFilePath = null;
5246
5785
  }
5247
5786
  if (realFilePath !== null) {
5248
- const realUiDir = fs35.realpathSync(this.uiDir);
5249
- if (!realFilePath.startsWith(realUiDir + path35.sep) && realFilePath !== realUiDir)
5787
+ const realUiDir = fs36.realpathSync(this.uiDir);
5788
+ if (!realFilePath.startsWith(realUiDir + path36.sep) && realFilePath !== realUiDir)
5250
5789
  return false;
5251
5790
  }
5252
- if (realFilePath !== null && fs35.existsSync(realFilePath) && fs35.statSync(realFilePath).isFile()) {
5253
- const ext = path35.extname(filePath);
5791
+ if (realFilePath !== null && fs36.existsSync(realFilePath) && fs36.statSync(realFilePath).isFile()) {
5792
+ const ext = path36.extname(filePath);
5254
5793
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
5255
5794
  const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
5256
5795
  const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
@@ -5258,16 +5797,16 @@ var init_static_server = __esm({
5258
5797
  "Content-Type": contentType,
5259
5798
  "Cache-Control": cacheControl
5260
5799
  });
5261
- fs35.createReadStream(realFilePath).pipe(res);
5800
+ fs36.createReadStream(realFilePath).pipe(res);
5262
5801
  return true;
5263
5802
  }
5264
- const indexPath = path35.join(this.uiDir, "index.html");
5265
- if (fs35.existsSync(indexPath)) {
5803
+ const indexPath = path36.join(this.uiDir, "index.html");
5804
+ if (fs36.existsSync(indexPath)) {
5266
5805
  res.writeHead(200, {
5267
5806
  "Content-Type": "text/html; charset=utf-8",
5268
5807
  "Cache-Control": "no-cache"
5269
5808
  });
5270
- fs35.createReadStream(indexPath).pipe(res);
5809
+ fs36.createReadStream(indexPath).pipe(res);
5271
5810
  return true;
5272
5811
  }
5273
5812
  return false;
@@ -5390,8 +5929,8 @@ var init_exports = __esm({
5390
5929
  });
5391
5930
 
5392
5931
  // src/plugins/context/context-cache.ts
5393
- import * as fs36 from "fs";
5394
- import * as path36 from "path";
5932
+ import * as fs37 from "fs";
5933
+ import * as path37 from "path";
5395
5934
  import * as crypto2 from "crypto";
5396
5935
  var DEFAULT_TTL_MS, ContextCache;
5397
5936
  var init_context_cache = __esm({
@@ -5402,7 +5941,7 @@ var init_context_cache = __esm({
5402
5941
  constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
5403
5942
  this.cacheDir = cacheDir;
5404
5943
  this.ttlMs = ttlMs;
5405
- fs36.mkdirSync(cacheDir, { recursive: true });
5944
+ fs37.mkdirSync(cacheDir, { recursive: true });
5406
5945
  }
5407
5946
  cacheDir;
5408
5947
  ttlMs;
@@ -5410,23 +5949,23 @@ var init_context_cache = __esm({
5410
5949
  return crypto2.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
5411
5950
  }
5412
5951
  filePath(repoPath, queryKey) {
5413
- return path36.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
5952
+ return path37.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
5414
5953
  }
5415
5954
  get(repoPath, queryKey) {
5416
5955
  const fp = this.filePath(repoPath, queryKey);
5417
5956
  try {
5418
- const stat = fs36.statSync(fp);
5957
+ const stat = fs37.statSync(fp);
5419
5958
  if (Date.now() - stat.mtimeMs > this.ttlMs) {
5420
- fs36.unlinkSync(fp);
5959
+ fs37.unlinkSync(fp);
5421
5960
  return null;
5422
5961
  }
5423
- return JSON.parse(fs36.readFileSync(fp, "utf-8"));
5962
+ return JSON.parse(fs37.readFileSync(fp, "utf-8"));
5424
5963
  } catch {
5425
5964
  return null;
5426
5965
  }
5427
5966
  }
5428
5967
  set(repoPath, queryKey, result) {
5429
- fs36.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
5968
+ fs37.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
5430
5969
  }
5431
5970
  };
5432
5971
  }
@@ -6400,8 +6939,8 @@ function formatToolSummary(name, rawInput, displaySummary) {
6400
6939
  }
6401
6940
  if (lowerName === "grep") {
6402
6941
  const pattern = args.pattern ?? "";
6403
- const path37 = args.path ?? "";
6404
- return pattern ? `\u{1F50D} Grep "${pattern}"${path37 ? ` in ${path37}` : ""}` : `\u{1F527} ${name}`;
6942
+ const path38 = args.path ?? "";
6943
+ return pattern ? `\u{1F50D} Grep "${pattern}"${path38 ? ` in ${path38}` : ""}` : `\u{1F527} ${name}`;
6405
6944
  }
6406
6945
  if (lowerName === "glob") {
6407
6946
  const pattern = args.pattern ?? "";
@@ -6437,8 +6976,8 @@ function formatToolTitle(name, rawInput, displayTitle) {
6437
6976
  }
6438
6977
  if (lowerName === "grep") {
6439
6978
  const pattern = args.pattern ?? "";
6440
- const path37 = args.path ?? "";
6441
- return pattern ? `"${pattern}"${path37 ? ` in ${path37}` : ""}` : name;
6979
+ const path38 = args.path ?? "";
6980
+ return pattern ? `"${pattern}"${path38 ? ` in ${path38}` : ""}` : name;
6442
6981
  }
6443
6982
  if (lowerName === "glob") {
6444
6983
  return String(args.pattern ?? name);
@@ -7890,7 +8429,7 @@ function setupDangerousModeCallbacks(bot, core) {
7890
8429
  }).catch(() => {
7891
8430
  });
7892
8431
  }
7893
- log24.info({ sessionId, wantOn }, "Bypass permissions toggled via button");
8432
+ log25.info({ sessionId, wantOn }, "Bypass permissions toggled via button");
7894
8433
  try {
7895
8434
  await ctx.editMessageText(buildSessionStatusText(session), {
7896
8435
  parse_mode: "HTML",
@@ -7913,7 +8452,7 @@ function setupDangerousModeCallbacks(bot, core) {
7913
8452
  const newDangerousMode = !(record.clientOverrides?.bypassPermissions ?? record.dangerousMode ?? false);
7914
8453
  core.sessionManager.patchRecord(sessionId, { clientOverrides: { bypassPermissions: newDangerousMode } }).catch(() => {
7915
8454
  });
7916
- log24.info(
8455
+ log25.info(
7917
8456
  { sessionId, dangerousMode: newDangerousMode },
7918
8457
  "Bypass permissions toggled via button (store-only, session not in memory)"
7919
8458
  );
@@ -8103,14 +8642,14 @@ async function handleRestart(ctx, core) {
8103
8642
  await new Promise((r) => setTimeout(r, 500));
8104
8643
  await core.requestRestart();
8105
8644
  }
8106
- var log24, OUTPUT_MODE_LABELS;
8645
+ var log25, OUTPUT_MODE_LABELS;
8107
8646
  var init_admin = __esm({
8108
8647
  "src/plugins/telegram/commands/admin.ts"() {
8109
8648
  "use strict";
8110
8649
  init_bypass_detection();
8111
8650
  init_formatting();
8112
8651
  init_log();
8113
- log24 = createChildLogger({ module: "telegram-cmd-admin" });
8652
+ log25 = createChildLogger({ module: "telegram-cmd-admin" });
8114
8653
  OUTPUT_MODE_LABELS = {
8115
8654
  low: "\u{1F507} Low",
8116
8655
  medium: "\u{1F4CA} Medium",
@@ -8125,7 +8664,7 @@ function botFromCtx(ctx) {
8125
8664
  return { api: ctx.api };
8126
8665
  }
8127
8666
  async function createSessionDirect(ctx, core, chatId, agentName, workspace, onControlMessage) {
8128
- log25.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
8667
+ log26.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
8129
8668
  let threadId;
8130
8669
  try {
8131
8670
  const topicName = `\u{1F504} New Session`;
@@ -8152,7 +8691,7 @@ async function createSessionDirect(ctx, core, chatId, agentName, workspace, onCo
8152
8691
  onControlMessage?.(session.id, controlMsg.message_id);
8153
8692
  return threadId ?? null;
8154
8693
  } catch (err) {
8155
- log25.error({ err }, "Session creation failed");
8694
+ log26.error({ err }, "Session creation failed");
8156
8695
  if (threadId) {
8157
8696
  try {
8158
8697
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -8385,7 +8924,7 @@ Agent: <code>${escapeHtml(agentKey)}</code>
8385
8924
  await _sendCustomPathPrompt(ctx, chatId, agentKey);
8386
8925
  });
8387
8926
  }
8388
- var log25, WS_CACHE_MAX, workspaceCache, nextWsId, _forceReplyMap;
8927
+ var log26, WS_CACHE_MAX, workspaceCache, nextWsId, _forceReplyMap;
8389
8928
  var init_new_session = __esm({
8390
8929
  "src/plugins/telegram/commands/new-session.ts"() {
8391
8930
  "use strict";
@@ -8393,7 +8932,7 @@ var init_new_session = __esm({
8393
8932
  init_topics();
8394
8933
  init_log();
8395
8934
  init_admin();
8396
- log25 = createChildLogger({ module: "telegram-cmd-new-session" });
8935
+ log26 = createChildLogger({ module: "telegram-cmd-new-session" });
8397
8936
  WS_CACHE_MAX = 50;
8398
8937
  workspaceCache = /* @__PURE__ */ new Map();
8399
8938
  nextWsId = 0;
@@ -8458,7 +8997,7 @@ ${lines.join("\n")}${truncated}`,
8458
8997
  { parse_mode: "HTML", reply_markup: keyboard }
8459
8998
  );
8460
8999
  } catch (err) {
8461
- log26.error({ err }, "handleTopics error");
9000
+ log27.error({ err }, "handleTopics error");
8462
9001
  await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
8463
9002
  });
8464
9003
  }
@@ -8479,13 +9018,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
8479
9018
  try {
8480
9019
  await ctx.api.deleteForumTopic(chatId, topicId);
8481
9020
  } catch (err) {
8482
- log26.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
9021
+ log27.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
8483
9022
  }
8484
9023
  }
8485
9024
  await core.sessionManager.removeRecord(record.sessionId);
8486
9025
  deleted++;
8487
9026
  } catch (err) {
8488
- log26.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
9027
+ log27.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
8489
9028
  failed++;
8490
9029
  }
8491
9030
  }
@@ -8556,7 +9095,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
8556
9095
  try {
8557
9096
  await core.sessionManager.cancelSession(record.sessionId);
8558
9097
  } catch (err) {
8559
- log26.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
9098
+ log27.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
8560
9099
  }
8561
9100
  }
8562
9101
  const topicId = record.platform?.topicId;
@@ -8564,13 +9103,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
8564
9103
  try {
8565
9104
  await ctx.api.deleteForumTopic(chatId, topicId);
8566
9105
  } catch (err) {
8567
- log26.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
9106
+ log27.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
8568
9107
  }
8569
9108
  }
8570
9109
  await core.sessionManager.removeRecord(record.sessionId);
8571
9110
  deleted++;
8572
9111
  } catch (err) {
8573
- log26.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
9112
+ log27.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
8574
9113
  failed++;
8575
9114
  }
8576
9115
  }
@@ -8638,13 +9177,13 @@ async function handleArchiveConfirm(ctx, core, chatId) {
8638
9177
  }
8639
9178
  }
8640
9179
  }
8641
- var log26;
9180
+ var log27;
8642
9181
  var init_session = __esm({
8643
9182
  "src/plugins/telegram/commands/session.ts"() {
8644
9183
  "use strict";
8645
9184
  init_formatting();
8646
9185
  init_log();
8647
- log26 = createChildLogger({ module: "telegram-cmd-session" });
9186
+ log27 = createChildLogger({ module: "telegram-cmd-session" });
8648
9187
  }
8649
9188
  });
8650
9189
 
@@ -9471,7 +10010,7 @@ var init_model = __esm({
9471
10010
  // src/plugins/telegram/commands/resume.ts
9472
10011
  function setupResumeCallbacks(_bot, _core, _chatId, _onControlMessage) {
9473
10012
  }
9474
- var log27;
10013
+ var log28;
9475
10014
  var init_resume = __esm({
9476
10015
  "src/plugins/telegram/commands/resume.ts"() {
9477
10016
  "use strict";
@@ -9481,7 +10020,38 @@ var init_resume = __esm({
9481
10020
  init_topics();
9482
10021
  init_admin();
9483
10022
  init_log();
9484
- log27 = createChildLogger({ module: "telegram-cmd-resume" });
10023
+ log28 = createChildLogger({ module: "telegram-cmd-resume" });
10024
+ }
10025
+ });
10026
+
10027
+ // src/plugins/telegram/callback-navigation.ts
10028
+ function contextualCommandCallback(command, target) {
10029
+ if (!command.startsWith("/")) throw new Error("Telegram navigation commands must start with /");
10030
+ const data = target === "settings" ? `${SETTINGS_PREFIX}${command}` : void 0;
10031
+ return data && Buffer.byteLength(data, "utf8") <= 64 ? data : void 0;
10032
+ }
10033
+ function settingsCommandCallback(command) {
10034
+ const data = contextualCommandCallback(command, "settings");
10035
+ if (!data) throw new Error("Telegram navigation callback exceeds 64 bytes");
10036
+ return data;
10037
+ }
10038
+ function decodeCommandCallback(data) {
10039
+ if (data.startsWith(SETTINGS_PREFIX)) {
10040
+ return { command: data.slice(SETTINGS_PREFIX.length), returnTarget: "settings" };
10041
+ }
10042
+ return { command: data.slice(2) };
10043
+ }
10044
+ function returnButton(target) {
10045
+ switch (target) {
10046
+ case "settings":
10047
+ return { text: "\u25C0\uFE0F Back to Settings", callback_data: "s:back:refresh" };
10048
+ }
10049
+ }
10050
+ var SETTINGS_PREFIX;
10051
+ var init_callback_navigation = __esm({
10052
+ "src/plugins/telegram/callback-navigation.ts"() {
10053
+ "use strict";
10054
+ SETTINGS_PREFIX = "c/@settings:";
9485
10055
  }
9486
10056
  });
9487
10057
 
@@ -9597,6 +10167,7 @@ async function buildSettingsKeyboard(core) {
9597
10167
  kb.text(`${label}`, `s:input:${field.path}`).row();
9598
10168
  }
9599
10169
  }
10170
+ kb.text("\u{1F310} Proxy Management", settingsCommandCallback("/proxy")).row();
9600
10171
  kb.text("\u25C0\uFE0F Back to Menu", "s:back");
9601
10172
  return kb;
9602
10173
  }
@@ -9644,7 +10215,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
9644
10215
  } catch {
9645
10216
  }
9646
10217
  } catch (err) {
9647
- log28.error({ err, fieldPath }, "Failed to toggle config");
10218
+ log29.error({ err, fieldPath }, "Failed to toggle config");
9648
10219
  try {
9649
10220
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
9650
10221
  } catch {
@@ -9725,7 +10296,7 @@ Tap to change:`, {
9725
10296
  } catch {
9726
10297
  }
9727
10298
  } catch (err) {
9728
- log28.error({ err, fieldPath }, "Failed to set config");
10299
+ log29.error({ err, fieldPath }, "Failed to set config");
9729
10300
  try {
9730
10301
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
9731
10302
  } catch {
@@ -9783,13 +10354,14 @@ Tap to change:`, {
9783
10354
  }
9784
10355
  });
9785
10356
  }
9786
- var log28;
10357
+ var log29;
9787
10358
  var init_settings = __esm({
9788
10359
  "src/plugins/telegram/commands/settings.ts"() {
9789
10360
  "use strict";
9790
10361
  init_config_registry();
9791
10362
  init_log();
9792
- log28 = createChildLogger({ module: "telegram-settings" });
10363
+ init_callback_navigation();
10364
+ log29 = createChildLogger({ module: "telegram-settings" });
9793
10365
  }
9794
10366
  });
9795
10367
 
@@ -9836,7 +10408,7 @@ async function handleDoctor(ctx, core) {
9836
10408
  reply_markup: keyboard
9837
10409
  });
9838
10410
  } catch (err) {
9839
- log29.error({ err }, "Doctor command failed");
10411
+ log30.error({ err }, "Doctor command failed");
9840
10412
  await ctx.api.editMessageText(
9841
10413
  ctx.chat.id,
9842
10414
  statusMsg.message_id,
@@ -9885,7 +10457,7 @@ function setupDoctorCallbacks(bot, core) {
9885
10457
  }
9886
10458
  }
9887
10459
  } catch (err) {
9888
- log29.error({ err, index }, "Doctor fix callback failed");
10460
+ log30.error({ err, index }, "Doctor fix callback failed");
9889
10461
  }
9890
10462
  });
9891
10463
  bot.callbackQuery("m:doctor", async (ctx) => {
@@ -9896,13 +10468,13 @@ function setupDoctorCallbacks(bot, core) {
9896
10468
  await handleDoctor(ctx, core);
9897
10469
  });
9898
10470
  }
9899
- var log29, pendingFixesStore;
10471
+ var log30, pendingFixesStore;
9900
10472
  var init_doctor2 = __esm({
9901
10473
  "src/plugins/telegram/commands/doctor.ts"() {
9902
10474
  "use strict";
9903
10475
  init_doctor();
9904
10476
  init_log();
9905
- log29 = createChildLogger({ module: "telegram-cmd-doctor" });
10477
+ log30 = createChildLogger({ module: "telegram-cmd-doctor" });
9906
10478
  pendingFixesStore = /* @__PURE__ */ new Map();
9907
10479
  }
9908
10480
  });
@@ -9961,13 +10533,13 @@ function setupTunnelCallbacks(bot, core) {
9961
10533
  }
9962
10534
  });
9963
10535
  }
9964
- var log30;
10536
+ var log31;
9965
10537
  var init_tunnel2 = __esm({
9966
10538
  "src/plugins/telegram/commands/tunnel.ts"() {
9967
10539
  "use strict";
9968
10540
  init_formatting();
9969
10541
  init_log();
9970
- log30 = createChildLogger({ module: "telegram-cmd-tunnel" });
10542
+ log31 = createChildLogger({ module: "telegram-cmd-tunnel" });
9971
10543
  }
9972
10544
  });
9973
10545
 
@@ -9981,10 +10553,10 @@ async function executeSwitchAgent(ctx, core, sessionId, agentName) {
9981
10553
  `Switched to <b>${escapeHtml(agentName)}</b> (${status})`,
9982
10554
  { parse_mode: "HTML" }
9983
10555
  );
9984
- log31.info({ sessionId, agentName, resumed }, "Agent switched via /switch");
10556
+ log32.info({ sessionId, agentName, resumed }, "Agent switched via /switch");
9985
10557
  } catch (err) {
9986
10558
  await ctx.reply(`Failed to switch agent: ${escapeHtml(String(err.message || err))}`);
9987
- log31.warn({ sessionId, agentName, err: err.message }, "Agent switch failed");
10559
+ log32.warn({ sessionId, agentName, err: err.message }, "Agent switch failed");
9988
10560
  }
9989
10561
  }
9990
10562
  function setupSwitchCallbacks(bot, core) {
@@ -10029,13 +10601,13 @@ Switch to <b>${escapeHtml(agentName)}</b> anyway?`,
10029
10601
  await executeSwitchAgent(ctx, core, session.id, data);
10030
10602
  });
10031
10603
  }
10032
- var log31;
10604
+ var log32;
10033
10605
  var init_switch = __esm({
10034
10606
  "src/plugins/telegram/commands/switch.ts"() {
10035
10607
  "use strict";
10036
10608
  init_formatting();
10037
10609
  init_log();
10038
- log31 = createChildLogger({ module: "telegram-cmd-switch" });
10610
+ log32 = createChildLogger({ module: "telegram-cmd-switch" });
10039
10611
  }
10040
10612
  });
10041
10613
 
@@ -10258,7 +10830,7 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
10258
10830
  if (!menuRegistry) return;
10259
10831
  const item = menuRegistry.getItem(itemId);
10260
10832
  if (!item) {
10261
- log32.warn({ itemId }, "Menu item not found in registry");
10833
+ log33.warn({ itemId }, "Menu item not found in registry");
10262
10834
  return;
10263
10835
  }
10264
10836
  const topicId = ctx.callbackQuery.message?.message_thread_id;
@@ -10344,7 +10916,7 @@ ${lines}`, { parse_mode: "HTML" }).catch(() => {
10344
10916
  }
10345
10917
  });
10346
10918
  }
10347
- var log32, STATIC_COMMANDS;
10919
+ var log33, STATIC_COMMANDS;
10348
10920
  var init_commands = __esm({
10349
10921
  "src/plugins/telegram/commands/index.ts"() {
10350
10922
  "use strict";
@@ -10370,7 +10942,7 @@ var init_commands = __esm({
10370
10942
  init_settings();
10371
10943
  init_doctor2();
10372
10944
  init_resume();
10373
- log32 = createChildLogger({ module: "telegram-menu-callbacks" });
10945
+ log33 = createChildLogger({ module: "telegram-menu-callbacks" });
10374
10946
  STATIC_COMMANDS = [
10375
10947
  { command: "new", description: "Create new session" },
10376
10948
  { command: "newchat", description: "New chat, same agent & workspace" },
@@ -10386,6 +10958,7 @@ var init_commands = __esm({
10386
10958
  { command: "restart", description: "Restart OpenACP" },
10387
10959
  { command: "update", description: "Update to latest version and restart" },
10388
10960
  { command: "doctor", description: "Run system diagnostics" },
10961
+ { command: "proxy", description: "Manage scoped proxy routing" },
10389
10962
  { command: "retry", description: "Re-check setup prerequisites (use if bot is stuck on startup)" },
10390
10963
  { command: "tunnel", description: "Create/stop tunnel for a local port" },
10391
10964
  { command: "tunnels", description: "List active tunnels" },
@@ -10406,14 +10979,14 @@ var init_commands = __esm({
10406
10979
  // src/plugins/telegram/permissions.ts
10407
10980
  import { InlineKeyboard as InlineKeyboard12 } from "grammy";
10408
10981
  import { nanoid as nanoid4 } from "nanoid";
10409
- var log33, PermissionHandler;
10982
+ var log34, PermissionHandler;
10410
10983
  var init_permissions = __esm({
10411
10984
  "src/plugins/telegram/permissions.ts"() {
10412
10985
  "use strict";
10413
10986
  init_formatting();
10414
10987
  init_topics();
10415
10988
  init_log();
10416
- log33 = createChildLogger({ module: "telegram-permissions" });
10989
+ log34 = createChildLogger({ module: "telegram-permissions" });
10417
10990
  PermissionHandler = class {
10418
10991
  constructor(bot, chatId, getSession, sendNotification) {
10419
10992
  this.bot = bot;
@@ -10491,7 +11064,7 @@ ${escapeHtml(request.description)}`,
10491
11064
  }
10492
11065
  const session = this.getSession(pending.sessionId);
10493
11066
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
10494
- log33.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
11067
+ log34.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
10495
11068
  if (session?.permissionGate.requestId === pending.requestId) {
10496
11069
  session.permissionGate.resolve(optionId);
10497
11070
  }
@@ -10511,7 +11084,7 @@ ${escapeHtml(request.description)}`,
10511
11084
  });
10512
11085
 
10513
11086
  // src/plugins/telegram/activity.ts
10514
- var log34, THINKING_REFRESH_MS, THINKING_MAX_MS, ThinkingIndicator, ToolCard, ActivityTracker2;
11087
+ var log35, THINKING_REFRESH_MS, THINKING_MAX_MS, ThinkingIndicator, ToolCard, ActivityTracker2;
10515
11088
  var init_activity = __esm({
10516
11089
  "src/plugins/telegram/activity.ts"() {
10517
11090
  "use strict";
@@ -10521,7 +11094,7 @@ var init_activity = __esm({
10521
11094
  init_stream_accumulator();
10522
11095
  init_stream_accumulator();
10523
11096
  init_display_spec_builder();
10524
- log34 = createChildLogger({ module: "telegram:activity" });
11097
+ log35 = createChildLogger({ module: "telegram:activity" });
10525
11098
  THINKING_REFRESH_MS = 15e3;
10526
11099
  THINKING_MAX_MS = 3 * 60 * 1e3;
10527
11100
  ThinkingIndicator = class {
@@ -10567,7 +11140,7 @@ var init_activity = __esm({
10567
11140
  }
10568
11141
  }
10569
11142
  } catch (err) {
10570
- log34.warn({ err }, "ThinkingIndicator.show() failed");
11143
+ log35.warn({ err }, "ThinkingIndicator.show() failed");
10571
11144
  } finally {
10572
11145
  this.sending = false;
10573
11146
  }
@@ -10744,7 +11317,7 @@ var init_activity = __esm({
10744
11317
  this.tracer?.log("telegram", { action: "telegram:delete:overflow", sessionId: this.sessionId, msgId: staleId });
10745
11318
  }
10746
11319
  } catch (err) {
10747
- log34.warn({ err }, "[ToolCard] send/edit failed");
11320
+ log35.warn({ err }, "[ToolCard] send/edit failed");
10748
11321
  }
10749
11322
  }
10750
11323
  };
@@ -10852,7 +11425,7 @@ var init_activity = __esm({
10852
11425
  const entry = this.toolStateMap.merge(id, status, rawInput, content, viewerLinks, diffStats);
10853
11426
  if (!existed || !entry) return;
10854
11427
  if (viewerLinks || entry.viewerLinks) {
10855
- log34.debug({ toolId: id, status, hasIncomingLinks: !!viewerLinks, hasEntryLinks: !!entry.viewerLinks, entryLinks: entry.viewerLinks }, "toolUpdate: viewer links trace");
11428
+ log35.debug({ toolId: id, status, hasIncomingLinks: !!viewerLinks, hasEntryLinks: !!entry.viewerLinks, entryLinks: entry.viewerLinks }, "toolUpdate: viewer links trace");
10856
11429
  }
10857
11430
  const spec = this.specBuilder.buildToolSpec(entry, this._outputMode, this.sessionContext);
10858
11431
  this.toolCard.updateFromSpec(spec);
@@ -11210,13 +11783,13 @@ var init_draft_manager = __esm({
11210
11783
  });
11211
11784
 
11212
11785
  // src/plugins/telegram/skill-command-manager.ts
11213
- var log35, SkillCommandManager;
11786
+ var log36, SkillCommandManager;
11214
11787
  var init_skill_command_manager = __esm({
11215
11788
  "src/plugins/telegram/skill-command-manager.ts"() {
11216
11789
  "use strict";
11217
11790
  init_commands();
11218
11791
  init_log();
11219
- log35 = createChildLogger({ module: "skill-commands" });
11792
+ log36 = createChildLogger({ module: "skill-commands" });
11220
11793
  SkillCommandManager = class {
11221
11794
  constructor(bot, chatId, sendQueue, sessionManager) {
11222
11795
  this.bot = bot;
@@ -11291,7 +11864,7 @@ var init_skill_command_manager = __esm({
11291
11864
  disable_notification: true
11292
11865
  });
11293
11866
  } catch (err) {
11294
- log35.error({ err, sessionId }, "Failed to send skill commands");
11867
+ log36.error({ err, sessionId }, "Failed to send skill commands");
11295
11868
  }
11296
11869
  }
11297
11870
  async cleanup(sessionId) {
@@ -11397,6 +11970,185 @@ var init_renderer2 = __esm({
11397
11970
  }
11398
11971
  });
11399
11972
 
11973
+ // src/plugins/identity/types.ts
11974
+ var init_types = __esm({
11975
+ "src/plugins/identity/types.ts"() {
11976
+ "use strict";
11977
+ }
11978
+ });
11979
+
11980
+ // src/core/commands/proxy.ts
11981
+ import { randomUUID as randomUUID3 } from "crypto";
11982
+ function clearProxyDraftsForChannel(channelId) {
11983
+ const prefix = `${channelId}:`;
11984
+ for (const [id, draft] of drafts) if (draft.owner.startsWith(prefix)) drafts.delete(id);
11985
+ }
11986
+ var DRAFT_TTL_MS, drafts;
11987
+ var init_proxy2 = __esm({
11988
+ "src/core/commands/proxy.ts"() {
11989
+ "use strict";
11990
+ init_proxy_types();
11991
+ init_types();
11992
+ init_proxy_store();
11993
+ init_proxy_service();
11994
+ DRAFT_TTL_MS = 10 * 6e4;
11995
+ drafts = /* @__PURE__ */ new Map();
11996
+ }
11997
+ });
11998
+
11999
+ // src/plugins/telegram/command-sync.ts
12000
+ import { createHash as createHash4 } from "crypto";
12001
+ function validDescription(description) {
12002
+ if (typeof description !== "string") return false;
12003
+ const trimmed = description.trim();
12004
+ return trimmed.length > 0 && trimmed.length <= TELEGRAM_DESCRIPTION_LIMIT;
12005
+ }
12006
+ function prepareTelegramCommandBoundary(coreCommands, registryCommands) {
12007
+ if (coreCommands.length > TELEGRAM_COMMAND_LIMIT) {
12008
+ throw new TelegramCommandBoundaryError("OpenACP core defines more than 100 Telegram commands");
12009
+ }
12010
+ const seen = /* @__PURE__ */ new Set();
12011
+ const commands = [];
12012
+ for (const command of coreCommands) {
12013
+ if (!TELEGRAM_COMMAND_NAME.test(command.command) || !validDescription(command.description)) {
12014
+ throw new TelegramCommandBoundaryError("OpenACP core contains an invalid Telegram command definition");
12015
+ }
12016
+ if (seen.has(command.command)) throw new TelegramCommandBoundaryError("OpenACP core contains a duplicate Telegram command definition");
12017
+ seen.add(command.command);
12018
+ commands.push({ command: command.command, description: command.description.trim() });
12019
+ }
12020
+ const skipped = { invalidName: 0, invalidDescription: 0, duplicate: 0, overflow: 0 };
12021
+ const validPlugins = [];
12022
+ for (const command of registryCommands) {
12023
+ if (command.category !== "plugin") continue;
12024
+ if (!TELEGRAM_COMMAND_NAME.test(command.name)) {
12025
+ skipped.invalidName++;
12026
+ continue;
12027
+ }
12028
+ if (!validDescription(command.description)) {
12029
+ skipped.invalidDescription++;
12030
+ continue;
12031
+ }
12032
+ if (seen.has(command.name) || validPlugins.some((entry) => entry.command === command.name)) {
12033
+ skipped.duplicate++;
12034
+ continue;
12035
+ }
12036
+ validPlugins.push({ command: command.name, description: command.description.trim() });
12037
+ }
12038
+ validPlugins.sort((a, b) => a.command.localeCompare(b.command) || a.description.localeCompare(b.description));
12039
+ const capacity = TELEGRAM_COMMAND_LIMIT - commands.length;
12040
+ skipped.overflow = Math.max(0, validPlugins.length - capacity);
12041
+ commands.push(...validPlugins.slice(0, capacity));
12042
+ return { commands, skipped };
12043
+ }
12044
+ function assertTelegramCommandBoundary(commands) {
12045
+ if (commands.length > TELEGRAM_COMMAND_LIMIT) throw new TelegramCommandBoundaryError("Telegram command list exceeds 100 entries");
12046
+ const seen = /* @__PURE__ */ new Set();
12047
+ for (const command of commands) {
12048
+ if (!TELEGRAM_COMMAND_NAME.test(command.command) || !validDescription(command.description) || seen.has(command.command)) {
12049
+ throw new TelegramCommandBoundaryError("Telegram command list failed validation before synchronization");
12050
+ }
12051
+ seen.add(command.command);
12052
+ }
12053
+ }
12054
+ function commandsEqual(current, desired) {
12055
+ return current.length === desired.length && current.every((command, index) => {
12056
+ const expected = desired[index];
12057
+ return command.command === expected?.command && command.description === expected.description;
12058
+ });
12059
+ }
12060
+ function throwIfAborted(signal) {
12061
+ if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
12062
+ }
12063
+ function localeLabel(locale) {
12064
+ return locale || "neutral";
12065
+ }
12066
+ async function synchronizeTelegramCommands(api, chatId, desired, options) {
12067
+ if (!/^\d{1,20}$/.test(options.botId)) throw new Error("Telegram bot identity is invalid");
12068
+ assertTelegramCommandBoundary(desired);
12069
+ const chatKey = createHash4("sha256").update(String(chatId)).digest("hex").slice(0, 16);
12070
+ const scopes = [
12071
+ { name: "default", scope: { type: "default" }, ledgerName: "default" },
12072
+ { name: "chat", scope: { type: "chat", chat_id: chatId }, ledgerName: `chat:${chatKey}` },
12073
+ {
12074
+ name: "chat_administrators",
12075
+ scope: { type: "chat_administrators", chat_id: chatId },
12076
+ ledgerName: `chat_administrators:${chatKey}`,
12077
+ onlyWhenPresent: true
12078
+ }
12079
+ ];
12080
+ const desiredNames = new Set(desired.map((command) => command.command));
12081
+ const result = { updated: [], unchanged: [] };
12082
+ const initialConservative = await options.ownershipStore.withLock(async ({ ledger, conservative }) => {
12083
+ options.ownershipStore.claimOwner(ledger, options.botId, options.ownerIdentity, options.allowOwnerTakeover);
12084
+ options.ownershipStore.save(ledger);
12085
+ return conservative;
12086
+ });
12087
+ options.onOwnerClaimed?.();
12088
+ for (const locale of TELEGRAM_COMMAND_LOCALES) {
12089
+ for (const entry of scopes) {
12090
+ throwIfAborted(options.signal);
12091
+ const label = `${entry.name}:${localeLabel(locale)}`;
12092
+ const scopeKey = `${entry.ledgerName}|${localeLabel(locale)}`;
12093
+ const apiOptions = {
12094
+ scope: entry.scope,
12095
+ ...locale ? { language_code: locale } : {}
12096
+ };
12097
+ const current = await api.getMyCommands(apiOptions, options.signal);
12098
+ throwIfAborted(options.signal);
12099
+ const ownership = await options.ownershipStore.withLock(async ({ ledger, conservative }) => ({
12100
+ priorOwned: options.ownershipStore.getOwned(ledger, options.botId, scopeKey),
12101
+ conservative
12102
+ }));
12103
+ if (entry.onlyWhenPresent && current.length === 0) {
12104
+ await options.ownershipStore.withLock(async ({ ledger }) => {
12105
+ options.ownershipStore.claimOwner(ledger, options.botId, options.ownerIdentity);
12106
+ options.ownershipStore.setOwned(ledger, options.botId, scopeKey, []);
12107
+ options.ownershipStore.save(ledger);
12108
+ });
12109
+ result.unchanged.push(label);
12110
+ continue;
12111
+ }
12112
+ const owned = new Set(ownership.priorOwned ?? (initialConservative || ownership.conservative ? [] : options.historicalOwnedNames));
12113
+ const unmanaged = current.filter((command) => !desiredNames.has(command.command) && !owned.has(command.command));
12114
+ const merged = [...desired, ...unmanaged];
12115
+ if (merged.length > 100) {
12116
+ throw new Error(`Telegram command scope ${label} has no capacity for OpenACP commands without deleting unmanaged commands`);
12117
+ }
12118
+ if (!commandsEqual(current, merged)) {
12119
+ throwIfAborted(options.signal);
12120
+ await api.setMyCommands(merged, apiOptions, options.signal);
12121
+ result.updated.push(label);
12122
+ } else {
12123
+ result.unchanged.push(label);
12124
+ }
12125
+ throwIfAborted(options.signal);
12126
+ await options.ownershipStore.withLock(async ({ ledger }) => {
12127
+ options.ownershipStore.claimOwner(ledger, options.botId, options.ownerIdentity);
12128
+ options.ownershipStore.setOwned(ledger, options.botId, scopeKey, [...desiredNames]);
12129
+ options.ownershipStore.save(ledger);
12130
+ });
12131
+ }
12132
+ }
12133
+ return result;
12134
+ }
12135
+ var TelegramCommandBoundaryError, TELEGRAM_COMMAND_NAME, TELEGRAM_COMMAND_LIMIT, TELEGRAM_DESCRIPTION_LIMIT;
12136
+ var init_command_sync = __esm({
12137
+ "src/plugins/telegram/command-sync.ts"() {
12138
+ "use strict";
12139
+ init_telegram_command_scopes();
12140
+ TelegramCommandBoundaryError = class extends Error {
12141
+ constructor(message) {
12142
+ super(message);
12143
+ this.name = "TelegramCommandBoundaryError";
12144
+ }
12145
+ };
12146
+ TELEGRAM_COMMAND_NAME = /^[a-z0-9_]{1,32}$/;
12147
+ TELEGRAM_COMMAND_LIMIT = 100;
12148
+ TELEGRAM_DESCRIPTION_LIMIT = 256;
12149
+ }
12150
+ });
12151
+
11400
12152
  // src/plugins/telegram/validators.ts
11401
12153
  var validators_exports = {};
11402
12154
  __export(validators_exports, {
@@ -11471,7 +12223,7 @@ async function validateBotAdmin(token, chatId, telegramFetch) {
11471
12223
  };
11472
12224
  }
11473
12225
  const { status } = data.result;
11474
- log36.info(
12226
+ log37.info(
11475
12227
  { status, can_manage_topics: data.result.can_manage_topics, raw_result: data.result },
11476
12228
  "validateBotAdmin: getChatMember raw result"
11477
12229
  );
@@ -11491,7 +12243,7 @@ async function validateBotAdmin(token, chatId, telegramFetch) {
11491
12243
  }
11492
12244
  async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
11493
12245
  const issues = [];
11494
- log36.info({ chatId }, "checkTopicsPrerequisites: starting checks");
12246
+ log37.info({ chatId }, "checkTopicsPrerequisites: starting checks");
11495
12247
  try {
11496
12248
  const res = await telegramFetch(`https://api.telegram.org/bot${token}/getChat`, {
11497
12249
  method: "POST",
@@ -11499,7 +12251,7 @@ async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
11499
12251
  body: JSON.stringify({ chat_id: chatId })
11500
12252
  });
11501
12253
  const data = await res.json();
11502
- log36.info(
12254
+ log37.info(
11503
12255
  { chatId, apiOk: data.ok, is_forum: data.result?.is_forum, type: data.result?.type, title: data.result?.title },
11504
12256
  "checkTopicsPrerequisites: getChat result"
11505
12257
  );
@@ -11509,11 +12261,11 @@ async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
11509
12261
  );
11510
12262
  }
11511
12263
  } catch (err) {
11512
- log36.warn({ err, chatId }, "checkTopicsPrerequisites: getChat failed (network error)");
12264
+ log37.warn({ err, chatId }, "checkTopicsPrerequisites: getChat failed (network error)");
11513
12265
  issues.push("\u274C Could not check if Topics are enabled (network error).");
11514
12266
  }
11515
12267
  const adminResult = await validateBotAdmin(token, chatId, telegramFetch);
11516
- log36.info(
12268
+ log37.info(
11517
12269
  { chatId, adminOk: adminResult.ok, canManageTopics: adminResult.ok ? adminResult.canManageTopics : void 0, error: !adminResult.ok ? adminResult.error : void 0 },
11518
12270
  "checkTopicsPrerequisites: validateBotAdmin result"
11519
12271
  );
@@ -11528,17 +12280,17 @@ async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
11528
12280
  '\u274C Bot cannot manage topics.\n\u2192 Go to Group Settings \u2192 Administrators \u2192 tap the bot \u2192 enable "Manage Topics" \u2192 tap Save/Done (the checkmark)'
11529
12281
  );
11530
12282
  }
11531
- log36.info({ chatId, issueCount: issues.length, ok: issues.length === 0 }, "checkTopicsPrerequisites: result");
12283
+ log37.info({ chatId, issueCount: issues.length, ok: issues.length === 0 }, "checkTopicsPrerequisites: result");
11532
12284
  if (issues.length > 0) return { ok: false, issues };
11533
12285
  return { ok: true };
11534
12286
  }
11535
- var log36;
12287
+ var log37;
11536
12288
  var init_validators = __esm({
11537
12289
  "src/plugins/telegram/validators.ts"() {
11538
12290
  "use strict";
11539
12291
  init_log();
11540
12292
  init_network_redaction();
11541
- log36 = createChildLogger({ module: "telegram-validators" });
12293
+ log37 = createChildLogger({ module: "telegram-validators" });
11542
12294
  }
11543
12295
  });
11544
12296
 
@@ -11557,7 +12309,7 @@ function patchedFetch(input2, init, delegate) {
11557
12309
  }
11558
12310
  return delegate(input2, init);
11559
12311
  }
11560
- var log37, TelegramAdapter;
12312
+ var log38, HISTORICAL_OPENACP_COMMAND_NAMES, TelegramAdapter;
11561
12313
  var init_adapter = __esm({
11562
12314
  "src/plugins/telegram/adapter.ts"() {
11563
12315
  "use strict";
@@ -11577,7 +12329,20 @@ var init_adapter = __esm({
11577
12329
  init_messaging_adapter();
11578
12330
  init_renderer2();
11579
12331
  init_output_mode_resolver();
11580
- log37 = createChildLogger({ module: "telegram" });
12332
+ init_proxy2();
12333
+ init_network_redaction();
12334
+ init_command_sync();
12335
+ init_command_ownership_store();
12336
+ init_instance_context();
12337
+ init_callback_navigation();
12338
+ log38 = createChildLogger({ module: "telegram" });
12339
+ HISTORICAL_OPENACP_COMMAND_NAMES = /* @__PURE__ */ new Set([
12340
+ ...STATIC_COMMANDS.map((command) => command.command),
12341
+ "clear",
12342
+ "bypass",
12343
+ "usage",
12344
+ "summary"
12345
+ ]);
11581
12346
  TelegramAdapter = class extends MessagingAdapter {
11582
12347
  name = "telegram";
11583
12348
  renderer = new TelegramRenderer();
@@ -11606,6 +12371,7 @@ var init_adapter = __esm({
11606
12371
  sessionTrackers = /* @__PURE__ */ new Map();
11607
12372
  callbackCache = /* @__PURE__ */ new Map();
11608
12373
  callbackCounter = 0;
12374
+ pendingCommandInputs = /* @__PURE__ */ new Map();
11609
12375
  /** Pending skill commands queued when session.threadId was not yet set */
11610
12376
  _pendingSkillCommands = /* @__PURE__ */ new Map();
11611
12377
  /** Control message IDs per session (for updating status text/buttons) */
@@ -11630,6 +12396,23 @@ var init_adapter = __esm({
11630
12396
  /** Set during normal shutdown so bot.stop() does not trigger a self-restart. */
11631
12397
  _stopping = false;
11632
12398
  unregisterProxyTester;
12399
+ /** Serializes command-list reconciliations triggered by startup and plugin hot reloads. */
12400
+ _commandSyncChain = Promise.resolve();
12401
+ _pendingCommandSnapshot;
12402
+ _commandSyncWorkerRunning = false;
12403
+ _commandSyncGeneration = 0;
12404
+ _commandSyncAbort;
12405
+ _commandSnapshotVersion = 0;
12406
+ _commandsReadyHandler;
12407
+ _latestCommandSnapshot;
12408
+ _commandApiReady = false;
12409
+ _initialCommandSyncScheduled = false;
12410
+ commandOwnershipStore = new TelegramCommandOwnershipStore(getGlobalRoot());
12411
+ commandOwnerIdentity;
12412
+ commandOwnerBotId;
12413
+ commandOwnerTakeoverRequested;
12414
+ _commandOwnerClaimed = false;
12415
+ _commandOwnerHeartbeat;
11633
12416
  telegramFetch() {
11634
12417
  return this.core.proxyService.createFetch("channels.telegram");
11635
12418
  }
@@ -11704,6 +12487,14 @@ var init_adapter = __esm({
11704
12487
  });
11705
12488
  this.core = core;
11706
12489
  this.telegramConfig = config;
12490
+ this.commandOwnerBotId = this.telegramConfig.botToken.split(":", 1)[0] ?? "";
12491
+ this.commandOwnerIdentity = {
12492
+ instanceId: this.core.instanceContext.id,
12493
+ instanceKey: telegramCommandInstanceKey(this.core.instanceContext.root),
12494
+ hostId: telegramCommandHostId(),
12495
+ pid: process.pid
12496
+ };
12497
+ this.commandOwnerTakeoverRequested = process.env.OPENACP_TELEGRAM_COMMAND_TAKEOVER === "1";
11707
12498
  this.saveTopicIds = saveTopicIds;
11708
12499
  this.unregisterProxyTester = this.core.proxyService.registerRouteTester(
11709
12500
  "channels.telegram",
@@ -11716,6 +12507,35 @@ var init_adapter = __esm({
11716
12507
  }
11717
12508
  );
11718
12509
  }
12510
+ /**
12511
+ * Capture command snapshots once startup begins. The normal boot path emits
12512
+ * SYSTEM_COMMANDS_READY before core.start(), so initial reconciliation reads the
12513
+ * authoritative registry state instead of relying on replay of that event. Once
12514
+ * the API is ready, later snapshots drive hot reloads.
12515
+ */
12516
+ subscribeToCommandSnapshots() {
12517
+ if (this._commandsReadyHandler) return;
12518
+ this._commandsReadyHandler = ({ commands }) => {
12519
+ this._latestCommandSnapshot = commands;
12520
+ if (this._commandApiReady) this.syncCommandsWithRetry(commands);
12521
+ };
12522
+ this.core.eventBus.on(BusEvent.SYSTEM_COMMANDS_READY, this._commandsReadyHandler);
12523
+ }
12524
+ /** Schedule one authoritative initial reconciliation after grammY is available. */
12525
+ scheduleInitialCommandSync() {
12526
+ if (this._initialCommandSyncScheduled) return;
12527
+ this._initialCommandSyncScheduled = true;
12528
+ const registry = this.core.lifecycleManager?.serviceRegistry?.get("command-registry");
12529
+ const commands = registry?.getAll() ?? this._latestCommandSnapshot;
12530
+ if (!commands) {
12531
+ log38.warn(
12532
+ { operation: "telegram-command-sync" },
12533
+ "Telegram command registry is not ready; waiting for a commands-ready snapshot"
12534
+ );
12535
+ return;
12536
+ }
12537
+ this.syncCommandsWithRetry(commands);
12538
+ }
11719
12539
  /**
11720
12540
  * Set up the grammY bot, register all callback and message handlers, then perform
11721
12541
  * two-phase startup: Phase 1 starts polling immediately; Phase 2 checks group
@@ -11724,6 +12544,11 @@ var init_adapter = __esm({
11724
12544
  */
11725
12545
  async start() {
11726
12546
  this._stopping = false;
12547
+ this._commandSyncGeneration++;
12548
+ this._commandSyncAbort = new AbortController();
12549
+ this._pendingCommandSnapshot = void 0;
12550
+ this._commandSyncWorkerRunning = false;
12551
+ this.subscribeToCommandSnapshots();
11727
12552
  this.bot = new Bot(this.telegramConfig.botToken, {
11728
12553
  client: {
11729
12554
  baseFetchConfig: { duplex: "half" },
@@ -11748,7 +12573,7 @@ var init_adapter = __esm({
11748
12573
  );
11749
12574
  this.bot.catch((err) => {
11750
12575
  const rootCause = err.error instanceof Error ? err.error : err;
11751
- log37.error({ err: rootCause }, "Telegram bot error");
12576
+ log38.error({ err: rootCause }, "Telegram bot error");
11752
12577
  });
11753
12578
  this.bot.api.config.use(async (prev, method, payload, signal) => {
11754
12579
  const maxRetries = 3;
@@ -11766,7 +12591,7 @@ var init_adapter = __esm({
11766
12591
  if (rateLimitedMethods.includes(method)) {
11767
12592
  this.sendQueue.onRateLimited();
11768
12593
  }
11769
- log37.warn(
12594
+ log38.warn(
11770
12595
  { method, retryAfter, attempt: attempt + 1 },
11771
12596
  "Rate limited by Telegram, retrying"
11772
12597
  );
@@ -11784,10 +12609,6 @@ var init_adapter = __esm({
11784
12609
  }
11785
12610
  return prev(method, payload, signal);
11786
12611
  });
11787
- const onCommandsReady = ({ commands }) => {
11788
- this.syncCommandsWithRetry(commands);
11789
- };
11790
- this.core.eventBus.on(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
11791
12612
  this.bot.use((ctx, next) => {
11792
12613
  const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
11793
12614
  if (chatId !== this.telegramConfig.chatId) return;
@@ -11801,7 +12622,58 @@ var init_adapter = __esm({
11801
12622
  );
11802
12623
  this.bot.on("message:text", async (ctx, next) => {
11803
12624
  const text3 = ctx.message?.text;
11804
- if (!text3?.startsWith("/")) return next();
12625
+ if (!text3) return next();
12626
+ const topicIdForInput = ctx.message.message_thread_id;
12627
+ const inputKey = `${ctx.chat.id}:${ctx.from?.id ?? 0}:${topicIdForInput ?? 0}`;
12628
+ const pending = this.pendingCommandInputs.get(inputKey);
12629
+ if (!text3.startsWith("/") && pending) {
12630
+ if (pending.promptMessageId !== void 0 && ctx.message.reply_to_message?.message_id !== pending.promptMessageId) {
12631
+ return next();
12632
+ }
12633
+ this.pendingCommandInputs.delete(inputKey);
12634
+ if (pending.expiresAt <= Date.now()) {
12635
+ await ctx.reply("This input request expired. Start the operation again.").catch(() => {
12636
+ });
12637
+ return;
12638
+ }
12639
+ if (pending.sensitive) {
12640
+ try {
12641
+ await ctx.deleteMessage();
12642
+ } catch {
12643
+ await ctx.reply(`\u26A0\uFE0F I could not securely remove that message, so its value was not used. ${pending.fallback}`).catch(() => {
12644
+ });
12645
+ return;
12646
+ }
12647
+ }
12648
+ const registry2 = this.core.lifecycleManager?.serviceRegistry?.get("command-registry");
12649
+ if (!registry2) return;
12650
+ const sessionId = topicIdForInput != null ? (await this.core.getOrResumeSession("telegram", String(topicIdForInput)))?.id ?? null : null;
12651
+ const response = await registry2.execute(pending.command, {
12652
+ raw: "",
12653
+ sessionId,
12654
+ channelId: "telegram",
12655
+ userId: String(ctx.from?.id),
12656
+ conversationId: `${ctx.chat.id}:${topicIdForInput ?? 0}`,
12657
+ interaction: {
12658
+ textInput: true,
12659
+ secureInput: "delete-after-capture",
12660
+ capturedInput: { value: text3, sensitive: pending.sensitive }
12661
+ },
12662
+ reply: async () => {
12663
+ }
12664
+ });
12665
+ if (response.type !== "silent" && response.type !== "delegated") {
12666
+ await this.renderCommandResponse(
12667
+ response,
12668
+ ctx.chat.id,
12669
+ topicIdForInput,
12670
+ String(ctx.from?.id),
12671
+ pending.returnTarget
12672
+ );
12673
+ }
12674
+ return;
12675
+ }
12676
+ if (!text3.startsWith("/")) return next();
11805
12677
  const rawCmd = text3.split(" ")[0].slice(1).split("@")[0].toLowerCase();
11806
12678
  if (rawCmd === "retry") {
11807
12679
  await this.handleRetryCommand(ctx);
@@ -11847,6 +12719,8 @@ var init_adapter = __esm({
11847
12719
  sessionId,
11848
12720
  channelId: "telegram",
11849
12721
  userId: String(ctx.from?.id),
12722
+ conversationId: `${chatId}:${topicId ?? 0}`,
12723
+ interaction: { textInput: true, secureInput: "delete-after-capture" },
11850
12724
  reply: async (content) => {
11851
12725
  if (typeof content === "string") {
11852
12726
  await ctx.reply(content);
@@ -11854,7 +12728,8 @@ var init_adapter = __esm({
11854
12728
  await this.renderCommandResponse(
11855
12729
  content,
11856
12730
  chatId,
11857
- topicId
12731
+ topicId,
12732
+ String(ctx.from?.id)
11858
12733
  );
11859
12734
  }
11860
12735
  }
@@ -11865,7 +12740,7 @@ var init_adapter = __esm({
11865
12740
  if (response.type === "silent") {
11866
12741
  return next();
11867
12742
  }
11868
- await this.renderCommandResponse(response, chatId, topicId);
12743
+ await this.renderCommandResponse(response, chatId, topicId, String(ctx.from?.id));
11869
12744
  } catch (err) {
11870
12745
  await ctx.reply(`\u26A0\uFE0F Command failed: ${String(err)}`);
11871
12746
  }
@@ -11877,7 +12752,8 @@ var init_adapter = __esm({
11877
12752
  return;
11878
12753
  }
11879
12754
  const data = ctx.callbackQuery.data;
11880
- const command = this.fromCallbackData(data);
12755
+ const callback = this.fromCallbackData(data);
12756
+ const command = callback.command;
11881
12757
  const registry = this.core.lifecycleManager?.serviceRegistry?.get(
11882
12758
  "command-registry"
11883
12759
  );
@@ -11894,6 +12770,8 @@ var init_adapter = __esm({
11894
12770
  sessionId,
11895
12771
  channelId: "telegram",
11896
12772
  userId: String(ctx.from?.id),
12773
+ conversationId: `${chatId}:${topicId ?? 0}`,
12774
+ interaction: { textInput: true, secureInput: "delete-after-capture" },
11897
12775
  reply: async (content) => {
11898
12776
  if (typeof content === "string") {
11899
12777
  await ctx.editMessageText(content).catch(() => {
@@ -11902,14 +12780,25 @@ var init_adapter = __esm({
11902
12780
  }
11903
12781
  });
11904
12782
  await ctx.answerCallbackQuery();
11905
- if (response.type !== "silent") {
12783
+ if (response.type !== "silent" && response.type !== "delegated") {
12784
+ if (response.type === "input") {
12785
+ await this.renderCommandResponse(
12786
+ response,
12787
+ chatId,
12788
+ topicId,
12789
+ String(ctx.from?.id),
12790
+ callback.returnTarget
12791
+ );
12792
+ return;
12793
+ }
11906
12794
  if (response.type === "menu") {
11907
12795
  const keyboard = response.options.map((opt) => [
11908
12796
  {
11909
12797
  text: `${opt.label}${opt.hint ? ` \u2014 ${opt.hint}` : ""}`,
11910
- callback_data: this.toCallbackData(opt.command)
12798
+ callback_data: this.toCallbackData(opt.command, callback.returnTarget)
11911
12799
  }
11912
12800
  ]);
12801
+ this.appendReturnButton(keyboard, callback.returnTarget);
11913
12802
  try {
11914
12803
  await ctx.editMessageText(response.title, {
11915
12804
  reply_markup: { inline_keyboard: keyboard }
@@ -11928,7 +12817,11 @@ var init_adapter = __esm({
11928
12817
  parseMode = "Markdown";
11929
12818
  }
11930
12819
  try {
11931
- await ctx.editMessageText(text3, { ...parseMode && { parse_mode: parseMode } });
12820
+ const returnMarkup = this.returnMarkup(callback.returnTarget);
12821
+ await ctx.editMessageText(text3, {
12822
+ ...parseMode && { parse_mode: parseMode },
12823
+ ...returnMarkup && { reply_markup: returnMarkup }
12824
+ });
11932
12825
  } catch {
11933
12826
  }
11934
12827
  }
@@ -12010,32 +12903,34 @@ ${p}` : p;
12010
12903
  }
12011
12904
  );
12012
12905
  this.setupRoutes();
12906
+ this._commandApiReady = true;
12907
+ this.scheduleInitialCommandSync();
12013
12908
  void this.bot.start({
12014
12909
  allowed_updates: ["message", "callback_query"],
12015
- onStart: () => log37.info({ chatId: this.telegramConfig.chatId }, "Telegram bot started")
12910
+ onStart: () => log38.info({ chatId: this.telegramConfig.chatId }, "Telegram bot started")
12016
12911
  }).catch((err) => {
12017
12912
  if (this._stopping) return;
12018
12913
  const inner = err?.error;
12019
12914
  const rootCause = err instanceof Error ? err : inner instanceof Error ? inner : err;
12020
- log37.error({ err: rootCause }, "Telegram polling stopped unexpectedly");
12915
+ log38.error({ err: rootCause }, "Telegram polling stopped unexpectedly");
12021
12916
  if (this.core.requestRestart) {
12022
12917
  const requestRestart = this.core.requestRestart;
12023
- log37.error("Restarting OpenACP because Telegram polling stopped");
12918
+ log38.error("Restarting OpenACP because Telegram polling stopped");
12024
12919
  setImmediate(() => {
12025
12920
  if (this._stopping) return;
12026
12921
  void requestRestart().catch((restartErr) => {
12027
- log37.error({ err: restartErr }, "OpenACP restart request failed after Telegram polling stopped");
12922
+ log38.error({ err: restartErr }, "OpenACP restart request failed after Telegram polling stopped");
12028
12923
  });
12029
12924
  });
12030
12925
  } else {
12031
- log37.error("Exiting because Telegram polling stopped and no restart hook is available");
12926
+ log38.error("Exiting because Telegram polling stopped and no restart hook is available");
12032
12927
  setImmediate(() => {
12033
12928
  if (this._stopping) return;
12034
12929
  process.exit(1);
12035
12930
  });
12036
12931
  }
12037
12932
  });
12038
- log37.info(
12933
+ log38.info(
12039
12934
  {
12040
12935
  chatId: this.telegramConfig.chatId,
12041
12936
  notificationTopicId: this.telegramConfig.notificationTopicId,
@@ -12050,12 +12945,12 @@ ${p}` : p;
12050
12945
  this.telegramFetch()
12051
12946
  );
12052
12947
  if (prereqResult.ok) {
12053
- log37.info("Telegram adapter: prerequisites OK, initializing topic-dependent features");
12948
+ log38.info("Telegram adapter: prerequisites OK, initializing topic-dependent features");
12054
12949
  await this.initTopicDependentFeatures();
12055
12950
  } else {
12056
- log37.warn({ issues: prereqResult.issues }, "Telegram adapter: prerequisites NOT met, starting watcher");
12951
+ log38.warn({ issues: prereqResult.issues }, "Telegram adapter: prerequisites NOT met, starting watcher");
12057
12952
  for (const issue of prereqResult.issues) {
12058
- log37.warn({ issue }, "Telegram prerequisite not met");
12953
+ log38.warn({ issue }, "Telegram prerequisite not met");
12059
12954
  }
12060
12955
  this.startPrerequisiteWatcher(prereqResult.issues);
12061
12956
  }
@@ -12071,7 +12966,7 @@ ${p}` : p;
12071
12966
  } catch (err) {
12072
12967
  if (attempt === maxRetries) throw err;
12073
12968
  const delay = baseDelayMs * Math.pow(2, attempt - 1);
12074
- log37.warn(
12969
+ log38.warn(
12075
12970
  { err, attempt, maxRetries, delayMs: delay, operation: label },
12076
12971
  `${label} failed, retrying in ${delay}ms`
12077
12972
  );
@@ -12086,21 +12981,126 @@ ${p}` : p;
12086
12981
  * from the registry, deduplicating by command name. Non-critical.
12087
12982
  */
12088
12983
  syncCommandsWithRetry(registryCommands) {
12089
- const staticNames = new Set(STATIC_COMMANDS.map((c2) => c2.command));
12090
- const pluginCommands = registryCommands.filter((c2) => c2.category === "plugin" && !staticNames.has(c2.name) && /^[a-z0-9_]+$/.test(c2.name)).map((c2) => ({ command: c2.name, description: c2.description.slice(0, 256) }));
12091
- const allCommands = [...STATIC_COMMANDS, ...pluginCommands].slice(0, 100);
12092
- this.retryWithBackoff(
12093
- () => this.bot.api.setMyCommands(allCommands, {
12094
- scope: { type: "chat", chat_id: this.telegramConfig.chatId }
12095
- }),
12096
- "setMyCommands"
12097
- ).catch((err) => {
12098
- log37.warn({ err }, "Failed to register Telegram commands after retries (non-critical)");
12984
+ this._pendingCommandSnapshot = registryCommands;
12985
+ this._commandSnapshotVersion++;
12986
+ if (this._commandSyncWorkerRunning) return;
12987
+ this._commandSyncWorkerRunning = true;
12988
+ const generation = this._commandSyncGeneration;
12989
+ const signal = this._commandSyncAbort?.signal;
12990
+ this._commandSyncChain = (async () => {
12991
+ while (this._pendingCommandSnapshot && generation === this._commandSyncGeneration && !signal?.aborted) {
12992
+ const snapshot = this._pendingCommandSnapshot;
12993
+ const snapshotVersion = this._commandSnapshotVersion;
12994
+ this._pendingCommandSnapshot = void 0;
12995
+ let allCommands;
12996
+ try {
12997
+ const boundary = prepareTelegramCommandBoundary(STATIC_COMMANDS, snapshot);
12998
+ allCommands = boundary.commands;
12999
+ const { invalidName, invalidDescription, duplicate, overflow } = boundary.skipped;
13000
+ if (invalidName || invalidDescription || duplicate || overflow) {
13001
+ log38.warn(
13002
+ { invalidName, invalidDescription, duplicate, overflow, operation: "telegram-command-boundary" },
13003
+ "Skipped plugin Telegram commands before Bot API sync. Names must match [a-z0-9_]{1,32}, descriptions must contain 1-256 characters, and only the first valid commands up to the 100-command limit are published."
13004
+ );
13005
+ }
13006
+ if (!allCommands.some((command) => command.command === "proxy")) {
13007
+ throw new TelegramCommandBoundaryError("Required OpenACP command /proxy is missing");
13008
+ }
13009
+ } catch (error) {
13010
+ log38.error(
13011
+ { error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)), operation: "telegram-command-boundary" },
13012
+ "OpenACP core Telegram command definitions are invalid; command sync was skipped without changing Telegram state"
13013
+ );
13014
+ continue;
13015
+ }
13016
+ const maxAttempts = 5;
13017
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
13018
+ if (generation !== this._commandSyncGeneration || signal?.aborted) return;
13019
+ try {
13020
+ const result = await synchronizeTelegramCommands(
13021
+ this.bot.api,
13022
+ this.telegramConfig.chatId,
13023
+ allCommands,
13024
+ {
13025
+ ownershipStore: this.commandOwnershipStore,
13026
+ botId: this.commandOwnerBotId,
13027
+ ownerIdentity: this.commandOwnerIdentity,
13028
+ allowOwnerTakeover: this.commandOwnerTakeoverRequested,
13029
+ onOwnerClaimed: () => {
13030
+ this._commandOwnerClaimed = true;
13031
+ this.startCommandOwnerHeartbeat();
13032
+ },
13033
+ historicalOwnedNames: HISTORICAL_OPENACP_COMMAND_NAMES,
13034
+ signal
13035
+ }
13036
+ );
13037
+ if (generation !== this._commandSyncGeneration || signal?.aborted) return;
13038
+ log38.info(
13039
+ { updatedScopes: result.updated, unchangedScopes: result.unchanged },
13040
+ "Telegram command menus synchronized"
13041
+ );
13042
+ break;
13043
+ } catch (err) {
13044
+ if (generation !== this._commandSyncGeneration || signal?.aborted) return;
13045
+ if (err instanceof TelegramCommandOwnerConflictError) {
13046
+ log38.warn(
13047
+ { code: err.code, ownerInstanceId: err.ownerInstanceId, instanceId: this.commandOwnerIdentity.instanceId, operation: "telegram-command-sync" },
13048
+ "Telegram command sync is owned by another OpenACP instance. This instance did not inspect or mutate Telegram command lists. Configure a unique bot per instance, or stop the same-host owner and request an explicit takeover."
13049
+ );
13050
+ break;
13051
+ }
13052
+ if (snapshotVersion !== this._commandSnapshotVersion && this._pendingCommandSnapshot) break;
13053
+ const safeError2 = redactNetworkSecrets(err instanceof Error ? err.message : String(err));
13054
+ if (attempt === maxAttempts) {
13055
+ log38.warn(
13056
+ { error: safeError2, attempt, maxAttempts, operation: "telegram-command-sync" },
13057
+ "Telegram command menu sync failed; bot remains online. Run /doctor and inspect Telegram connectivity"
13058
+ );
13059
+ break;
13060
+ }
13061
+ const delayMs = 2e3 * Math.pow(2, attempt - 1);
13062
+ log38.warn(
13063
+ { error: safeError2, attempt, maxAttempts, delayMs, operation: "telegram-command-sync" },
13064
+ "Telegram command menu sync failed, retrying"
13065
+ );
13066
+ await new Promise((resolve7) => {
13067
+ const timer = setTimeout(resolve7, delayMs);
13068
+ signal?.addEventListener("abort", () => {
13069
+ clearTimeout(timer);
13070
+ resolve7();
13071
+ }, { once: true });
13072
+ });
13073
+ }
13074
+ }
13075
+ }
13076
+ })().finally(() => {
13077
+ this._commandSyncWorkerRunning = false;
13078
+ if (this._pendingCommandSnapshot && generation === this._commandSyncGeneration && !signal?.aborted) {
13079
+ this.syncCommandsWithRetry(this._pendingCommandSnapshot);
13080
+ }
12099
13081
  });
12100
13082
  }
13083
+ startCommandOwnerHeartbeat() {
13084
+ if (this._commandOwnerHeartbeat) return;
13085
+ this._commandOwnerHeartbeat = setInterval(() => {
13086
+ void this.commandOwnershipStore.heartbeatOwner(this.commandOwnerBotId, this.commandOwnerIdentity).then((owned) => {
13087
+ if (!owned) {
13088
+ this._commandOwnerClaimed = false;
13089
+ log38.warn(
13090
+ { instanceId: this.commandOwnerIdentity.instanceId, operation: "telegram-command-owner-heartbeat" },
13091
+ "Telegram command-sync ownership was lost; no further command-list writes will be attempted until ownership is resolved."
13092
+ );
13093
+ }
13094
+ }).catch((error) => log38.warn(
13095
+ { error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)), operation: "telegram-command-owner-heartbeat" },
13096
+ "Could not refresh Telegram command-sync ownership heartbeat"
13097
+ ));
13098
+ }, 3e4);
13099
+ this._commandOwnerHeartbeat.unref?.();
13100
+ }
12101
13101
  async initTopicDependentFeatures() {
12102
13102
  if (this._topicsInitialized) return;
12103
- log37.info(
13103
+ log38.info(
12104
13104
  { notificationTopicId: this.telegramConfig.notificationTopicId, assistantTopicId: this.telegramConfig.assistantTopicId },
12105
13105
  "initTopicDependentFeatures: starting (existing IDs in config)"
12106
13106
  );
@@ -12125,7 +13125,7 @@ ${p}` : p;
12125
13125
  this.assistantTopicId = topics.assistantTopicId;
12126
13126
  this._systemTopicIds.notificationTopicId = topics.notificationTopicId;
12127
13127
  this._systemTopicIds.assistantTopicId = topics.assistantTopicId;
12128
- log37.info(
13128
+ log38.info(
12129
13129
  { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId },
12130
13130
  "initTopicDependentFeatures: topics ready"
12131
13131
  );
@@ -12156,7 +13156,7 @@ ${p}` : p;
12156
13156
  ).then((msg) => {
12157
13157
  if (msg) this.storeControlMsgId(sessionId, msg.message_id);
12158
13158
  }).catch((err) => {
12159
- log37.warn({ err, sessionId }, "Failed to send initial messages for new session");
13159
+ log38.warn({ err, sessionId }, "Failed to send initial messages for new session");
12160
13160
  });
12161
13161
  };
12162
13162
  this.core.eventBus.on(BusEvent.SESSION_THREAD_READY, this._threadReadyHandler);
@@ -12198,7 +13198,7 @@ ${p}` : p;
12198
13198
  this._queueNotifications.delete(data.turnId);
12199
13199
  if (result) {
12200
13200
  this.bot.api.deleteMessage(this.telegramConfig.chatId, result.message_id).catch((err) => {
12201
- log37.warn({ err }, "Failed to delete queue notification after race");
13201
+ log38.warn({ err }, "Failed to delete queue notification after race");
12202
13202
  });
12203
13203
  }
12204
13204
  } else if (result) {
@@ -12208,7 +13208,7 @@ ${p}` : p;
12208
13208
  }
12209
13209
  } catch (err) {
12210
13210
  this._queueNotifications.delete(data.turnId);
12211
- log37.warn({ err }, "Failed to send queue notification");
13211
+ log38.warn({ err }, "Failed to send queue notification");
12212
13212
  }
12213
13213
  });
12214
13214
  this.core.eventBus.on(BusEvent.MESSAGE_PROCESSING, async (data) => {
@@ -12217,13 +13217,13 @@ ${p}` : p;
12217
13217
  if (typeof entry === "number") {
12218
13218
  this._queueNotifications.delete(data.turnId);
12219
13219
  this.bot.api.deleteMessage(this.telegramConfig.chatId, entry).catch((err) => {
12220
- log37.warn({ err }, "Failed to delete queue notification on processing");
13220
+ log38.warn({ err }, "Failed to delete queue notification on processing");
12221
13221
  });
12222
13222
  } else if (entry === "pending") {
12223
13223
  this._queueNotifications.set(data.turnId, "delete");
12224
13224
  }
12225
13225
  });
12226
- log37.info({ assistantTopicId: this.assistantTopicId }, "initTopicDependentFeatures: sending welcome message");
13226
+ log38.info({ assistantTopicId: this.assistantTopicId }, "initTopicDependentFeatures: sending welcome message");
12227
13227
  try {
12228
13228
  const config = this.core.configManager.get();
12229
13229
  const agents = this.core.agentManager.getAvailableAgents();
@@ -12245,17 +13245,17 @@ ${p}` : p;
12245
13245
  this.core.lifecycleManager?.serviceRegistry?.get("menu-registry")
12246
13246
  )
12247
13247
  });
12248
- log37.info("initTopicDependentFeatures: welcome message sent");
13248
+ log38.info("initTopicDependentFeatures: welcome message sent");
12249
13249
  } catch (err) {
12250
- log37.warn({ err }, "Failed to send welcome message");
13250
+ log38.warn({ err }, "Failed to send welcome message");
12251
13251
  }
12252
13252
  try {
12253
13253
  await this.core.assistantManager.getOrSpawn("telegram", String(this.assistantTopicId));
12254
13254
  } catch (err) {
12255
- log37.error({ err }, "Failed to spawn assistant");
13255
+ log38.error({ err }, "Failed to spawn assistant");
12256
13256
  }
12257
13257
  this._topicsInitialized = true;
12258
- log37.info("Telegram adapter fully initialized");
13258
+ log38.info("Telegram adapter fully initialized");
12259
13259
  }
12260
13260
  /**
12261
13261
  * Handle the /retry command — re-runs prerequisite checks immediately.
@@ -12292,7 +13292,7 @@ ${p}` : p;
12292
13292
  ).catch(() => {
12293
13293
  });
12294
13294
  } catch (err) {
12295
- log37.error({ err }, "handleRetryCommand: init failed after prerequisites met");
13295
+ log38.error({ err }, "handleRetryCommand: init failed after prerequisites met");
12296
13296
  await ctx.reply("\u26A0\uFE0F Prerequisites are met but initialization failed. Check server logs and try /retry again.").catch(() => {
12297
13297
  });
12298
13298
  }
@@ -12318,7 +13318,7 @@ OpenACP will automatically retry every 30 seconds. After fixing the issues above
12318
13318
  this.bot.api.sendMessage(this.telegramConfig.chatId, setupMessage, {
12319
13319
  parse_mode: "HTML"
12320
13320
  }).catch((err) => {
12321
- log37.warn({ err }, "Failed to send setup guidance to General topic");
13321
+ log38.warn({ err }, "Failed to send setup guidance to General topic");
12322
13322
  });
12323
13323
  const schedule = [5e3, 1e4, 3e4];
12324
13324
  let attempt = 1;
@@ -12331,7 +13331,7 @@ OpenACP will automatically retry every 30 seconds. After fixing the issues above
12331
13331
  this.telegramFetch()
12332
13332
  );
12333
13333
  if (result.ok) {
12334
- log37.info("Prerequisites met \u2014 completing Telegram adapter initialization");
13334
+ log38.info("Prerequisites met \u2014 completing Telegram adapter initialization");
12335
13335
  try {
12336
13336
  await this.initTopicDependentFeatures();
12337
13337
  this._prerequisiteWatcher = null;
@@ -12344,14 +13344,14 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12344
13344
  { parse_mode: "HTML" }
12345
13345
  );
12346
13346
  } catch (err) {
12347
- log37.error({ err }, "Failed to complete initialization after prerequisites met \u2014 will retry");
13347
+ log38.error({ err }, "Failed to complete initialization after prerequisites met \u2014 will retry");
12348
13348
  const delay2 = schedule[Math.min(attempt, schedule.length - 1)];
12349
13349
  attempt++;
12350
13350
  this._prerequisiteWatcher = setTimeout(retry, delay2);
12351
13351
  }
12352
13352
  return;
12353
13353
  }
12354
- log37.debug({ issues: result.issues }, "Prerequisites not yet met, retrying");
13354
+ log38.debug({ issues: result.issues }, "Prerequisites not yet met, retrying");
12355
13355
  const delay = schedule[Math.min(attempt, schedule.length - 1)];
12356
13356
  attempt++;
12357
13357
  this._prerequisiteWatcher = setTimeout(retry, delay);
@@ -12367,6 +13367,14 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12367
13367
  */
12368
13368
  async stop() {
12369
13369
  this._stopping = true;
13370
+ this._commandSyncGeneration++;
13371
+ this._commandSyncAbort?.abort();
13372
+ this._commandSyncAbort = void 0;
13373
+ this._pendingCommandSnapshot = void 0;
13374
+ if (this._commandOwnerHeartbeat) {
13375
+ clearInterval(this._commandOwnerHeartbeat);
13376
+ this._commandOwnerHeartbeat = void 0;
13377
+ }
12370
13378
  this.unregisterProxyTester?.();
12371
13379
  this.unregisterProxyTester = void 0;
12372
13380
  if (this._prerequisiteWatcher !== null) {
@@ -12385,16 +13393,42 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12385
13393
  this.core.eventBus.off("session:configChanged", this._configChangedHandler);
12386
13394
  this._configChangedHandler = void 0;
12387
13395
  }
13396
+ if (this._commandsReadyHandler) {
13397
+ this.core.eventBus.off(BusEvent.SYSTEM_COMMANDS_READY, this._commandsReadyHandler);
13398
+ this._commandsReadyHandler = void 0;
13399
+ }
13400
+ this._commandApiReady = false;
13401
+ this._initialCommandSyncScheduled = false;
13402
+ this._latestCommandSnapshot = void 0;
12388
13403
  this.sendQueue.clear();
12389
- await this.bot.stop();
12390
- log37.info("Telegram bot stopped");
13404
+ this.pendingCommandInputs.clear();
13405
+ this.callbackCache.clear();
13406
+ clearProxyDraftsForChannel("telegram");
13407
+ try {
13408
+ await this.bot.stop();
13409
+ } finally {
13410
+ if (this._commandOwnerClaimed) {
13411
+ try {
13412
+ await this.commandOwnershipStore.releaseOwner(this.commandOwnerBotId, this.commandOwnerIdentity);
13413
+ } catch (error) {
13414
+ log38.warn(
13415
+ { error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)), operation: "telegram-command-owner-release" },
13416
+ "Could not mark Telegram command-sync ownership as stopped; a same-host explicit takeover may be required on restart"
13417
+ );
13418
+ }
13419
+ this._commandOwnerClaimed = false;
13420
+ }
13421
+ }
13422
+ log38.info("Telegram bot stopped");
12391
13423
  }
12392
13424
  // --- CommandRegistry response rendering ---
12393
- async renderCommandResponse(response, chatId, topicId) {
13425
+ async renderCommandResponse(response, chatId, topicId, userId, returnTarget) {
13426
+ const returnMarkup = this.returnMarkup(returnTarget);
12394
13427
  switch (response.type) {
12395
13428
  case "text":
12396
13429
  await this.bot.api.sendMessage(chatId, response.text, {
12397
- message_thread_id: topicId
13430
+ message_thread_id: topicId,
13431
+ ...returnMarkup && { reply_markup: returnMarkup }
12398
13432
  });
12399
13433
  break;
12400
13434
  case "adaptive": {
@@ -12402,7 +13436,8 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12402
13436
  const text3 = variant?.text ?? response.fallback;
12403
13437
  await this.bot.api.sendMessage(chatId, text3, {
12404
13438
  message_thread_id: topicId,
12405
- ...variant?.parse_mode && { parse_mode: variant.parse_mode }
13439
+ ...variant?.parse_mode && { parse_mode: variant.parse_mode },
13440
+ ...returnMarkup && { reply_markup: returnMarkup }
12406
13441
  });
12407
13442
  break;
12408
13443
  }
@@ -12410,16 +13445,17 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12410
13445
  await this.bot.api.sendMessage(
12411
13446
  chatId,
12412
13447
  `\u26A0\uFE0F ${response.message}`,
12413
- { message_thread_id: topicId }
13448
+ { message_thread_id: topicId, ...returnMarkup && { reply_markup: returnMarkup } }
12414
13449
  );
12415
13450
  break;
12416
13451
  case "menu": {
12417
13452
  const keyboard = response.options.map((opt) => [
12418
13453
  {
12419
13454
  text: `${opt.label}${opt.hint ? ` \u2014 ${opt.hint}` : ""}`,
12420
- callback_data: this.toCallbackData(opt.command)
13455
+ callback_data: this.toCallbackData(opt.command, returnTarget)
12421
13456
  }
12422
13457
  ]);
13458
+ this.appendReturnButton(keyboard, returnTarget);
12423
13459
  await this.bot.api.sendMessage(chatId, response.title, {
12424
13460
  message_thread_id: topicId,
12425
13461
  reply_markup: { inline_keyboard: keyboard }
@@ -12433,7 +13469,8 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12433
13469
  const text3 = `${response.title}
12434
13470
  ${lines.join("\n")}`;
12435
13471
  await this.bot.api.sendMessage(chatId, text3, {
12436
- message_thread_id: topicId
13472
+ message_thread_id: topicId,
13473
+ ...returnMarkup && { reply_markup: returnMarkup }
12437
13474
  });
12438
13475
  break;
12439
13476
  }
@@ -12442,31 +13479,68 @@ ${lines.join("\n")}`;
12442
13479
  [
12443
13480
  {
12444
13481
  text: "\u2705 Yes",
12445
- callback_data: this.toCallbackData(response.onYes)
13482
+ callback_data: this.toCallbackData(response.onYes, returnTarget)
12446
13483
  }
12447
13484
  ]
12448
13485
  ];
12449
13486
  if (response.onNo) {
12450
13487
  buttons[0].push({
12451
13488
  text: "\u274C No",
12452
- callback_data: this.toCallbackData(response.onNo)
13489
+ callback_data: this.toCallbackData(response.onNo, returnTarget)
12453
13490
  });
12454
13491
  }
13492
+ this.appendReturnButton(buttons, returnTarget);
12455
13493
  await this.bot.api.sendMessage(chatId, response.question, {
12456
13494
  message_thread_id: topicId,
12457
13495
  reply_markup: { inline_keyboard: buttons }
12458
13496
  });
12459
13497
  break;
12460
13498
  }
13499
+ case "input": {
13500
+ if (!userId) {
13501
+ await this.bot.api.sendMessage(chatId, response.fallback, {
13502
+ message_thread_id: topicId,
13503
+ ...returnMarkup && { reply_markup: returnMarkup }
13504
+ });
13505
+ break;
13506
+ }
13507
+ const key = `${chatId}:${userId}:${topicId ?? 0}`;
13508
+ for (const [pendingKey, pending] of this.pendingCommandInputs) {
13509
+ if (pending.expiresAt <= Date.now()) this.pendingCommandInputs.delete(pendingKey);
13510
+ }
13511
+ while (this.pendingCommandInputs.size >= 1e3) {
13512
+ const oldest = this.pendingCommandInputs.keys().next().value;
13513
+ if (!oldest) break;
13514
+ this.pendingCommandInputs.delete(oldest);
13515
+ }
13516
+ const warning = response.sensitive ? "\n\n\u{1F510} Your reply will be deleted before the value is processed. If deletion fails, the value will not be used." : "";
13517
+ const promptMessage = await this.bot.api.sendMessage(chatId, `${response.prompt}${warning}`, {
13518
+ message_thread_id: topicId,
13519
+ reply_markup: { force_reply: true, selective: true }
13520
+ });
13521
+ this.pendingCommandInputs.set(key, {
13522
+ command: response.command,
13523
+ sensitive: response.sensitive === true,
13524
+ fallback: response.fallback,
13525
+ expiresAt: Date.now() + (response.expiresInMs ?? 10 * 6e4),
13526
+ promptMessageId: promptMessage.message_id,
13527
+ returnTarget
13528
+ });
13529
+ break;
13530
+ }
12461
13531
  case "silent":
12462
13532
  break;
12463
13533
  }
12464
13534
  }
12465
- toCallbackData(command) {
13535
+ toCallbackData(command, returnTarget) {
12466
13536
  const data = `c/${command}`;
12467
- if (data.length <= 64) return data;
13537
+ if (!returnTarget && Buffer.byteLength(data, "utf8") <= 64) return data;
13538
+ if (returnTarget) {
13539
+ const contextual = contextualCommandCallback(command, returnTarget);
13540
+ if (contextual) return contextual;
13541
+ }
12468
13542
  const id = String(++this.callbackCounter);
12469
- this.callbackCache.set(id, command);
13543
+ this.callbackCache.set(id, { command, returnTarget });
12470
13544
  if (this.callbackCache.size > 1e3) {
12471
13545
  const first = this.callbackCache.keys().next().value;
12472
13546
  if (first) this.callbackCache.delete(first);
@@ -12475,9 +13549,15 @@ ${lines.join("\n")}`;
12475
13549
  }
12476
13550
  fromCallbackData(data) {
12477
13551
  if (data.startsWith("c/#")) {
12478
- return this.callbackCache.get(data.slice(3)) ?? data.slice(2);
13552
+ return this.callbackCache.get(data.slice(3)) ?? { command: data.slice(2) };
12479
13553
  }
12480
- return data.slice(2);
13554
+ return decodeCommandCallback(data);
13555
+ }
13556
+ returnMarkup(returnTarget) {
13557
+ return returnTarget ? { inline_keyboard: [[returnButton(returnTarget)]] } : void 0;
13558
+ }
13559
+ appendReturnButton(keyboard, returnTarget) {
13560
+ if (returnTarget) keyboard.push([returnButton(returnTarget)]);
12481
13561
  }
12482
13562
  setupRoutes() {
12483
13563
  this.bot.on("message:text", async (ctx) => {
@@ -12532,7 +13612,7 @@ ${lines.join("\n")}`;
12532
13612
  username: ctx.from.username
12533
13613
  }
12534
13614
  }
12535
- ).catch((err) => log37.error({ err }, "handleMessage error"));
13615
+ ).catch((err) => log38.error({ err }, "handleMessage error"));
12536
13616
  });
12537
13617
  this.bot.on("message:photo", async (ctx) => {
12538
13618
  const threadId = ctx.message.message_thread_id;
@@ -12638,7 +13718,7 @@ ${lines.join("\n")}`;
12638
13718
  if (session.archiving) return;
12639
13719
  const threadId = Number(session.threadId);
12640
13720
  if (!threadId || isNaN(threadId)) {
12641
- log37.warn(
13721
+ log38.warn(
12642
13722
  { sessionId, threadId: session.threadId },
12643
13723
  "Session has no valid threadId, skipping message"
12644
13724
  );
@@ -12654,7 +13734,7 @@ ${lines.join("\n")}`;
12654
13734
  this._sessionThreadIds.delete(sessionId);
12655
13735
  }
12656
13736
  }).catch((err) => {
12657
- log37.warn({ err, sessionId }, "Dispatch queue error");
13737
+ log38.warn({ err, sessionId }, "Dispatch queue error");
12658
13738
  });
12659
13739
  this._dispatchQueues.set(sessionId, next);
12660
13740
  await next;
@@ -12784,7 +13864,7 @@ ${lines.join("\n")}`;
12784
13864
  usageMsgId = result?.message_id;
12785
13865
  if (usageMsgId) this.usageMsgIds.set(sessionId, usageMsgId);
12786
13866
  } catch (err) {
12787
- log37.warn({ err, sessionId }, "Failed to send usage message");
13867
+ log38.warn({ err, sessionId }, "Failed to send usage message");
12788
13868
  }
12789
13869
  }
12790
13870
  async handleAttachment(sessionId, content) {
@@ -12793,7 +13873,7 @@ ${lines.join("\n")}`;
12793
13873
  if (!content.attachment) return;
12794
13874
  const { attachment } = content;
12795
13875
  if (attachment.size > 50 * 1024 * 1024) {
12796
- log37.warn(
13876
+ log38.warn(
12797
13877
  {
12798
13878
  sessionId,
12799
13879
  fileName: attachment.fileName,
@@ -12837,7 +13917,7 @@ ${lines.join("\n")}`;
12837
13917
  );
12838
13918
  }
12839
13919
  } catch (err) {
12840
- log37.error(
13920
+ log38.error(
12841
13921
  { err, sessionId, fileName: attachment.fileName },
12842
13922
  "Failed to send attachment"
12843
13923
  );
@@ -12962,7 +14042,7 @@ Task completed.
12962
14042
  */
12963
14043
  async sendPermissionRequest(sessionId, request) {
12964
14044
  this.getTracer(sessionId)?.log("telegram", { action: "permission:send", sessionId, requestId: request.id, description: request.description });
12965
- log37.info({ sessionId, requestId: request.id }, "Permission request sent");
14045
+ log38.info({ sessionId, requestId: request.id }, "Permission request sent");
12966
14046
  const session = this.core.sessionManager.getSession(sessionId);
12967
14047
  if (!session) return;
12968
14048
  await this.sendQueue.enqueue(
@@ -12977,7 +14057,7 @@ Task completed.
12977
14057
  async sendNotification(notification) {
12978
14058
  this.getTracer(notification.sessionId)?.log("telegram", { action: "notification:send", sessionId: notification.sessionId, type: notification.type });
12979
14059
  if (notification.sessionId === this.core.assistantManager?.get("telegram")?.id) return;
12980
- log37.info(
14060
+ log38.info(
12981
14061
  { sessionId: notification.sessionId, type: notification.type },
12982
14062
  "Notification sent"
12983
14063
  );
@@ -13023,14 +14103,14 @@ Task completed.
13023
14103
  */
13024
14104
  async createSessionThread(sessionId, name) {
13025
14105
  this.getTracer(sessionId)?.log("telegram", { action: "thread:create", sessionId, name });
13026
- log37.info({ sessionId, name }, "Session topic created");
14106
+ log38.info({ sessionId, name }, "Session topic created");
13027
14107
  const threadId = await createSessionTopic(this.bot, this.telegramConfig.chatId, name);
13028
14108
  this.bot.api.sendMessage(
13029
14109
  this.telegramConfig.chatId,
13030
14110
  "\u23F3 Setting up session, please wait...",
13031
14111
  { message_thread_id: threadId, parse_mode: "HTML" }
13032
14112
  ).catch((err) => {
13033
- log37.warn({ err, sessionId, threadId }, "Failed to send setup message to new topic");
14113
+ log38.warn({ err, sessionId, threadId }, "Failed to send setup message to new topic");
13034
14114
  });
13035
14115
  return String(threadId);
13036
14116
  }
@@ -13044,7 +14124,7 @@ Task completed.
13044
14124
  if (!session) return;
13045
14125
  const threadId = Number(session.threadId);
13046
14126
  if (!threadId) {
13047
- log37.debug({ sessionId, newName }, "Cannot rename thread \u2014 threadId not set yet");
14127
+ log38.debug({ sessionId, newName }, "Cannot rename thread \u2014 threadId not set yet");
13048
14128
  return;
13049
14129
  }
13050
14130
  await renameSessionTopic(
@@ -13064,7 +14144,7 @@ Task completed.
13064
14144
  try {
13065
14145
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
13066
14146
  } catch (err) {
13067
- log37.warn(
14147
+ log38.warn(
13068
14148
  { err, sessionId, topicId },
13069
14149
  "Failed to delete forum topic (may already be deleted)"
13070
14150
  );
@@ -13113,7 +14193,7 @@ Task completed.
13113
14193
  const buffer = Buffer.from(await response.arrayBuffer());
13114
14194
  return { buffer, filePath: file.file_path };
13115
14195
  } catch (err) {
13116
- log37.error({ err }, "Failed to download file from Telegram");
14196
+ log38.error({ err }, "Failed to download file from Telegram");
13117
14197
  return null;
13118
14198
  }
13119
14199
  }
@@ -13134,7 +14214,7 @@ Task completed.
13134
14214
  try {
13135
14215
  buffer = await this.fileService.convertOggToWav(buffer);
13136
14216
  } catch (err) {
13137
- log37.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
14217
+ log38.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
13138
14218
  fileName = "voice.ogg";
13139
14219
  mimeType = "audio/ogg";
13140
14220
  originalFilePath = void 0;
@@ -13169,7 +14249,7 @@ Task completed.
13169
14249
  userId: String(userId),
13170
14250
  text: text3,
13171
14251
  attachments: [att]
13172
- }).catch((err) => log37.error({ err }, "handleMessage error"));
14252
+ }).catch((err) => log38.error({ err }, "handleMessage error"));
13173
14253
  }
13174
14254
  /**
13175
14255
  * Remove skill slash commands from the Telegram bot command list for a session.
@@ -15275,7 +16355,7 @@ var TTS_BLOCK_REGEX = /\[TTS\]([\s\S]*?)\[\/TTS\]/;
15275
16355
  var TTS_MAX_LENGTH = 5e3;
15276
16356
  var TTS_TIMEOUT_MS = 3e4;
15277
16357
  var VALID_TRANSITIONS = {
15278
- initializing: /* @__PURE__ */ new Set(["active", "error"]),
16358
+ initializing: /* @__PURE__ */ new Set(["active", "error", "cancelled"]),
15279
16359
  active: /* @__PURE__ */ new Set(["error", "finished", "cancelled"]),
15280
16360
  error: /* @__PURE__ */ new Set(["active", "cancelled"]),
15281
16361
  cancelled: /* @__PURE__ */ new Set(["active"]),
@@ -15337,6 +16417,9 @@ var Session = class extends TypedEmitter {
15337
16417
  _abortedTurnIds = /* @__PURE__ */ new Set();
15338
16418
  /** Last completed turnId — used by abortPrompt() to retroactively mark a just-finished turn as interrupted */
15339
16419
  _lastCompletedTurnId = null;
16420
+ /** Idempotent teardown checkpoints allow cleanup retry after a partial failure. */
16421
+ _destroyedAgent = false;
16422
+ _closedLogger = false;
15340
16423
  _autoApprovedCommands = [];
15341
16424
  get autoApprovedCommands() {
15342
16425
  return this._autoApprovedCommands;
@@ -15415,7 +16498,7 @@ var Session = class extends TypedEmitter {
15415
16498
  this.transition("finished");
15416
16499
  this.emit(SessionEv.SESSION_END, reason ?? "completed");
15417
16500
  }
15418
- /** Transition to cancelled — from active or error (terminal session cancel) */
16501
+ /** Transition to cancelled — from initializing, active, or error. */
15419
16502
  markCancelled() {
15420
16503
  this.transition("cancelled");
15421
16504
  }
@@ -15932,8 +17015,14 @@ ${result.text}` : result.text;
15932
17015
  this.permissionGate.reject("Session destroyed");
15933
17016
  }
15934
17017
  this.queue.clear();
15935
- await this.agentInstance.destroy();
15936
- await closeSessionLogger(this.log);
17018
+ if (!this._destroyedAgent) {
17019
+ await this.agentInstance.destroy();
17020
+ this._destroyedAgent = true;
17021
+ }
17022
+ if (!this._closedLogger) {
17023
+ await closeSessionLogger(this.log);
17024
+ this._closedLogger = true;
17025
+ }
15937
17026
  }
15938
17027
  };
15939
17028
 
@@ -16451,6 +17540,9 @@ var MessageTransformer = class {
16451
17540
 
16452
17541
  // src/core/sessions/session-manager.ts
16453
17542
  init_events();
17543
+ init_log();
17544
+ init_network_redaction();
17545
+ var log7 = createChildLogger({ module: "session-manager" });
16454
17546
  var SessionManager = class {
16455
17547
  sessions = /* @__PURE__ */ new Map();
16456
17548
  store;
@@ -16460,6 +17552,7 @@ var SessionManager = class {
16460
17552
  // Prevents SessionBridge STATUS_CHANGE listeners from overwriting the flushed state
16461
17553
  // with transient error status from in-flight prompts that fail after shutdown.
16462
17554
  _shutdownComplete = false;
17555
+ cancellationOps = /* @__PURE__ */ new Map();
16463
17556
  /**
16464
17557
  * Inject the EventBus after construction. Deferred because EventBus is created
16465
17558
  * after SessionManager during bootstrap, so it cannot be passed to the constructor.
@@ -16560,30 +17653,76 @@ var SessionManager = class {
16560
17653
  getSessionRecord(sessionId) {
16561
17654
  return this.store?.get(sessionId);
16562
17655
  }
16563
- /** Cancel a session: abort in-flight prompt, transition to cancelled, destroy agent, and persist. */
17656
+ /**
17657
+ * Cancel a session exactly once. Concurrent callers share one operation;
17658
+ * terminal sessions return an idempotent result instead of throwing.
17659
+ */
16564
17660
  async cancelSession(sessionId) {
17661
+ const existing = this.cancellationOps.get(sessionId);
17662
+ if (existing) return existing;
17663
+ const operation = this.performCancelSession(sessionId);
17664
+ this.cancellationOps.set(sessionId, operation);
17665
+ try {
17666
+ return await operation;
17667
+ } finally {
17668
+ if (this.cancellationOps.get(sessionId) === operation) this.cancellationOps.delete(sessionId);
17669
+ }
17670
+ }
17671
+ async performCancelSession(sessionId) {
16565
17672
  const session = this.sessions.get(sessionId);
16566
- if (session) {
16567
- try {
16568
- await session.abortPrompt();
16569
- } catch {
16570
- }
16571
- if (session.status !== "finished" && session.status !== "cancelled") {
16572
- session.markCancelled();
16573
- }
16574
- await session.destroy();
16575
- this.sessions.delete(sessionId);
17673
+ const recordBefore = this.store?.get(sessionId);
17674
+ const previousStatus = session?.status ?? recordBefore?.status;
17675
+ if (!previousStatus) {
17676
+ const error = new Error(`Session "${sessionId}" not found`);
17677
+ error.code = "SESSION_NOT_FOUND";
17678
+ throw error;
16576
17679
  }
16577
- if (this.store) {
17680
+ const alreadyTerminal = previousStatus === "cancelled" || previousStatus === "finished";
17681
+ const finalStatus = previousStatus === "finished" ? "finished" : "cancelled";
17682
+ if (this.store && finalStatus === "cancelled") {
16578
17683
  const record = this.store.get(sessionId);
16579
- if (record && record.status !== "cancelled" && record.status !== "finished") {
17684
+ if (record && record.status !== "cancelled") {
16580
17685
  await this.store.save({ ...record, status: "cancelled" });
17686
+ this.store.flush();
16581
17687
  }
16582
17688
  }
16583
- if (this.middlewareChain) {
17689
+ let cleanupPending = false;
17690
+ if (session) {
17691
+ if (!alreadyTerminal) {
17692
+ session.markCancelled();
17693
+ try {
17694
+ await session.abortPrompt();
17695
+ } catch (error) {
17696
+ log7.warn(
17697
+ { sessionId, error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)) },
17698
+ "Session prompt abort failed after cancellation became durable; continuing cleanup"
17699
+ );
17700
+ }
17701
+ }
17702
+ try {
17703
+ await session.destroy();
17704
+ this.sessions.delete(sessionId);
17705
+ } catch (error) {
17706
+ cleanupPending = true;
17707
+ log7.warn(
17708
+ { sessionId, error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)) },
17709
+ "Session is durably cancelled but agent/logger cleanup failed; repeat cancellation to retry cleanup"
17710
+ );
17711
+ }
17712
+ }
17713
+ if (!cleanupPending && this.middlewareChain) {
16584
17714
  this.middlewareChain.execute(Hook.SESSION_AFTER_DESTROY, { sessionId }, async (p) => p).catch(() => {
16585
17715
  });
16586
17716
  }
17717
+ return {
17718
+ sessionId,
17719
+ cancelled: !alreadyTerminal,
17720
+ previousStatus,
17721
+ status: finalStatus,
17722
+ alreadyTerminal,
17723
+ cleanupPending,
17724
+ ...cleanupPending ? { warning: "Session state is cancelled and persisted; process cleanup is pending. Repeat cancellation to retry cleanup." } : {}
17725
+ };
16587
17726
  }
16588
17727
  /** List live (in-memory) sessions, optionally filtered by channel. Excludes assistant sessions. */
16589
17728
  listSessions(channelId) {
@@ -16752,7 +17891,7 @@ init_bypass_detection();
16752
17891
  import micromatch from "micromatch";
16753
17892
  init_events();
16754
17893
  var { isMatch } = micromatch;
16755
- var log7 = createChildLogger({ module: "session-bridge" });
17894
+ var log8 = createChildLogger({ module: "session-bridge" });
16756
17895
  var SessionBridge = class {
16757
17896
  constructor(session, adapter, deps, adapterId) {
16758
17897
  this.session = session;
@@ -16784,16 +17923,16 @@ var SessionBridge = class {
16784
17923
  if (!result) return;
16785
17924
  this.tracer?.log("core", { step: "dispatch", sessionId, message: result.message });
16786
17925
  this.adapter.sendMessage(sessionId, result.message).catch((err) => {
16787
- log7.error({ err, sessionId }, "Failed to send message to adapter");
17926
+ log8.error({ err, sessionId }, "Failed to send message to adapter");
16788
17927
  });
16789
17928
  } else {
16790
17929
  this.tracer?.log("core", { step: "dispatch", sessionId, message });
16791
17930
  this.adapter.sendMessage(sessionId, message).catch((err) => {
16792
- log7.error({ err, sessionId }, "Failed to send message to adapter");
17931
+ log8.error({ err, sessionId }, "Failed to send message to adapter");
16793
17932
  });
16794
17933
  }
16795
17934
  } catch (err) {
16796
- log7.error({ err, sessionId }, "Error in sendMessage middleware");
17935
+ log8.error({ err, sessionId }, "Error in sendMessage middleware");
16797
17936
  }
16798
17937
  }
16799
17938
  /**
@@ -16839,7 +17978,7 @@ var SessionBridge = class {
16839
17978
  try {
16840
17979
  await this.adapter.sendPermissionRequest(this.session.id, request);
16841
17980
  } catch (err) {
16842
- log7.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
17981
+ log8.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
16843
17982
  }
16844
17983
  });
16845
17984
  this.listen(this.session, SessionEv.STATUS_CHANGE, (from, to) => {
@@ -16924,14 +18063,14 @@ var SessionBridge = class {
16924
18063
  try {
16925
18064
  this.handleAgentEvent(event);
16926
18065
  } catch (err) {
16927
- log7.error({ err, sessionId: this.session.id }, "Error handling agent event (middleware fallback)");
18066
+ log8.error({ err, sessionId: this.session.id }, "Error handling agent event (middleware fallback)");
16928
18067
  }
16929
18068
  }
16930
18069
  } else {
16931
18070
  try {
16932
18071
  this.handleAgentEvent(event);
16933
18072
  } catch (err) {
16934
- log7.error({ err, sessionId: this.session.id }, "Error handling agent event");
18073
+ log8.error({ err, sessionId: this.session.id }, "Error handling agent event");
16935
18074
  }
16936
18075
  }
16937
18076
  }
@@ -16984,40 +18123,40 @@ var SessionBridge = class {
16984
18123
  break;
16985
18124
  case "image_content": {
16986
18125
  if (this.deps.fileService) {
16987
- const fs37 = this.deps.fileService;
18126
+ const fs38 = this.deps.fileService;
16988
18127
  const sid = this.session.id;
16989
18128
  const { data, mimeType } = event;
16990
18129
  const buffer = Buffer.from(data, "base64");
16991
- const ext = fs37.extensionFromMime(mimeType);
16992
- fs37.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
18130
+ const ext = fs38.extensionFromMime(mimeType);
18131
+ fs38.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
16993
18132
  this.sendMessage(sid, {
16994
18133
  type: "attachment",
16995
18134
  text: "",
16996
18135
  attachment: att
16997
18136
  });
16998
- }).catch((err) => log7.error({ err }, "Failed to save agent image"));
18137
+ }).catch((err) => log8.error({ err }, "Failed to save agent image"));
16999
18138
  }
17000
18139
  break;
17001
18140
  }
17002
18141
  case "audio_content": {
17003
18142
  if (this.deps.fileService) {
17004
- const fs37 = this.deps.fileService;
18143
+ const fs38 = this.deps.fileService;
17005
18144
  const sid = this.session.id;
17006
18145
  const { data, mimeType } = event;
17007
18146
  const buffer = Buffer.from(data, "base64");
17008
- const ext = fs37.extensionFromMime(mimeType);
17009
- fs37.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
18147
+ const ext = fs38.extensionFromMime(mimeType);
18148
+ fs38.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
17010
18149
  this.sendMessage(sid, {
17011
18150
  type: "attachment",
17012
18151
  text: "",
17013
18152
  attachment: att
17014
18153
  });
17015
- }).catch((err) => log7.error({ err }, "Failed to save agent audio"));
18154
+ }).catch((err) => log8.error({ err }, "Failed to save agent audio"));
17016
18155
  }
17017
18156
  break;
17018
18157
  }
17019
18158
  case "commands_update":
17020
- log7.debug({ commands: event.commands }, "Commands available");
18159
+ log8.debug({ commands: event.commands }, "Commands available");
17021
18160
  this.adapter.sendSkillCommands?.(this.session.id, event.commands);
17022
18161
  break;
17023
18162
  case "system_message":
@@ -17114,7 +18253,7 @@ var SessionBridge = class {
17114
18253
  if (isAgentBypass || isClientBypass) {
17115
18254
  const allowOption = request.options.find((o) => o.isAllow);
17116
18255
  if (allowOption) {
17117
- log7.info(
18256
+ log8.info(
17118
18257
  { sessionId: this.session.id, requestId: request.id, optionId: allowOption.id, agentBypass: !!isAgentBypass, clientBypass: !!isClientBypass },
17119
18258
  "Bypass mode: auto-approving permission"
17120
18259
  );
@@ -17127,7 +18266,7 @@ var SessionBridge = class {
17127
18266
  if (isMatch(normalized, patterns, { dot: true })) {
17128
18267
  const allowOption = request.options.find((o) => o.isAllow);
17129
18268
  if (allowOption) {
17130
- log7.info(
18269
+ log8.info(
17131
18270
  { sessionId: this.session.id, requestId: request.id, command: request.description },
17132
18271
  "autoApprovedCommands: auto-approving matching command"
17133
18272
  );
@@ -17155,7 +18294,7 @@ var SessionBridge = class {
17155
18294
  // src/core/sessions/session-factory.ts
17156
18295
  init_log();
17157
18296
  init_events();
17158
- var log8 = createChildLogger({ module: "session-factory" });
18297
+ var log9 = createChildLogger({ module: "session-factory" });
17159
18298
  function toSetConfigOptionValue(option) {
17160
18299
  return option.type === "boolean" ? { type: "boolean", value: option.currentValue } : { type: "select", value: option.currentValue };
17161
18300
  }
@@ -17229,7 +18368,7 @@ var SessionFactory = class {
17229
18368
  configAllowedPaths
17230
18369
  );
17231
18370
  } catch (resumeErr) {
17232
- log8.warn(
18371
+ log9.warn(
17233
18372
  { agentName: createParams.agentName, resumeErr },
17234
18373
  "Agent session resume failed, falling back to fresh spawn"
17235
18374
  );
@@ -17382,10 +18521,10 @@ var SessionFactory = class {
17382
18521
  }
17383
18522
  }
17384
18523
  }
17385
- log8.info({ sessionId }, "Lazy resume by ID successful");
18524
+ log9.info({ sessionId }, "Lazy resume by ID successful");
17386
18525
  return session;
17387
18526
  } catch (err) {
17388
- log8.error({ err, sessionId }, "Lazy resume by ID failed");
18527
+ log9.error({ err, sessionId }, "Lazy resume by ID failed");
17389
18528
  return null;
17390
18529
  } finally {
17391
18530
  this.resumeLocks.delete(sessionId);
@@ -17410,19 +18549,19 @@ var SessionFactory = class {
17410
18549
  (p) => String(p.topicId) === threadId || String(p.threadId ?? "") === threadId
17411
18550
  );
17412
18551
  if (!record) {
17413
- log8.debug({ threadId, channelId }, "No session record found for thread");
18552
+ log9.debug({ threadId, channelId }, "No session record found for thread");
17414
18553
  return null;
17415
18554
  }
17416
18555
  if (record.isAssistant) return null;
17417
18556
  if (record.status === "error" || record.status === "cancelled") {
17418
- log8.warn(
18557
+ log9.warn(
17419
18558
  { threadId, sessionId: record.sessionId, status: record.status },
17420
18559
  "Session record found but skipped (status: %s) \u2014 use /new to start a fresh session",
17421
18560
  record.status
17422
18561
  );
17423
18562
  return null;
17424
18563
  }
17425
- log8.info({ threadId, sessionId: record.sessionId, status: record.status }, "Lazy resume: found record, attempting resume");
18564
+ log9.info({ threadId, sessionId: record.sessionId, status: record.status }, "Lazy resume: found record, attempting resume");
17426
18565
  const resumePromise = (async () => {
17427
18566
  try {
17428
18567
  const session = await this.createFullSession({
@@ -17474,7 +18613,7 @@ var SessionFactory = class {
17474
18613
  }
17475
18614
  const resumeFalledBack = record.agentSessionId && session.agentSessionId !== record.agentSessionId;
17476
18615
  if (resumeFalledBack) {
17477
- log8.info({ sessionId: session.id }, "Resume fell back to fresh spawn \u2014 injecting conversation history");
18616
+ log9.info({ sessionId: session.id }, "Resume fell back to fresh spawn \u2014 injecting conversation history");
17478
18617
  const contextManager = this.getContextManager?.();
17479
18618
  if (contextManager) {
17480
18619
  try {
@@ -17491,10 +18630,10 @@ var SessionFactory = class {
17491
18630
  }
17492
18631
  }
17493
18632
  }
17494
- log8.info({ sessionId: session.id, threadId }, "Lazy resume successful");
18633
+ log9.info({ sessionId: session.id, threadId }, "Lazy resume successful");
17495
18634
  return session;
17496
18635
  } catch (err) {
17497
- log8.error({ err, record }, "Lazy resume failed");
18636
+ log9.error({ err, record }, "Lazy resume failed");
17498
18637
  const adapter = this.adapters?.get(channelId);
17499
18638
  if (adapter) {
17500
18639
  try {
@@ -17520,7 +18659,7 @@ var SessionFactory = class {
17520
18659
  }
17521
18660
  const config = this.configManager.get();
17522
18661
  const resolvedAgent = agentName || config.defaultAgent;
17523
- log8.info({ channelId, agentName: resolvedAgent }, "New session request");
18662
+ log9.info({ channelId, agentName: resolvedAgent }, "New session request");
17524
18663
  const agentDef = this.agentCatalog.resolve(resolvedAgent);
17525
18664
  const resolvedWorkspace = this.configManager.resolveWorkspace(
17526
18665
  workspacePath || agentDef?.workingDirectory
@@ -17570,7 +18709,7 @@ var SessionFactory = class {
17570
18709
  params.contextOptions
17571
18710
  );
17572
18711
  } catch (err) {
17573
- log8.warn({ err }, "Context building failed, proceeding without context");
18712
+ log9.warn({ err }, "Context building failed, proceeding without context");
17574
18713
  }
17575
18714
  }
17576
18715
  const session = await this.createFullSession({
@@ -17625,7 +18764,7 @@ import { nanoid as nanoid3 } from "nanoid";
17625
18764
  init_log();
17626
18765
  import fs9 from "fs";
17627
18766
  import path8 from "path";
17628
- var log9 = createChildLogger({ module: "session-store" });
18767
+ var log10 = createChildLogger({ module: "session-store" });
17629
18768
  var DEBOUNCE_MS = 2e3;
17630
18769
  var JsonFileSessionStore = class {
17631
18770
  records = /* @__PURE__ */ new Map();
@@ -17739,7 +18878,7 @@ var JsonFileSessionStore = class {
17739
18878
  fs9.readFileSync(this.filePath, "utf-8")
17740
18879
  );
17741
18880
  if (raw.version !== 1) {
17742
- log9.warn(
18881
+ log10.warn(
17743
18882
  { version: raw.version },
17744
18883
  "Unknown session store version, skipping load"
17745
18884
  );
@@ -17748,9 +18887,9 @@ var JsonFileSessionStore = class {
17748
18887
  for (const [id, record] of Object.entries(raw.sessions)) {
17749
18888
  this.records.set(id, this.migrateRecord(record));
17750
18889
  }
17751
- log9.debug({ count: this.records.size }, "Loaded session records");
18890
+ log10.debug({ count: this.records.size }, "Loaded session records");
17752
18891
  } catch (err) {
17753
- log9.error({ err }, "Failed to load session store, backing up corrupt file");
18892
+ log10.error({ err }, "Failed to load session store, backing up corrupt file");
17754
18893
  try {
17755
18894
  fs9.renameSync(this.filePath, `${this.filePath}.bak`);
17756
18895
  } catch {
@@ -17793,7 +18932,7 @@ var JsonFileSessionStore = class {
17793
18932
  }
17794
18933
  }
17795
18934
  if (removed > 0) {
17796
- log9.info({ removed }, "Cleaned up expired session records");
18935
+ log10.info({ removed }, "Cleaned up expired session records");
17797
18936
  this.scheduleDiskWrite();
17798
18937
  }
17799
18938
  }
@@ -17812,7 +18951,7 @@ init_agent_registry();
17812
18951
  init_agent_registry();
17813
18952
  init_log();
17814
18953
  init_events();
17815
- var log10 = createChildLogger({ module: "agent-switch" });
18954
+ var log11 = createChildLogger({ module: "agent-switch" });
17816
18955
  var AgentSwitchHandler = class {
17817
18956
  constructor(deps) {
17818
18957
  this.deps = deps;
@@ -17893,7 +19032,7 @@ var AgentSwitchHandler = class {
17893
19032
  resumed = true;
17894
19033
  return instance2;
17895
19034
  } catch {
17896
- log10.warn({ sessionId, toAgent }, "Resume failed, falling back to new agent with context injection");
19035
+ log11.warn({ sessionId, toAgent }, "Resume failed, falling back to new agent with context injection");
17897
19036
  }
17898
19037
  }
17899
19038
  const instance = await agentManager.spawn(toAgent, session.workingDirectory, configAllowedPaths);
@@ -17962,10 +19101,10 @@ var AgentSwitchHandler = class {
17962
19101
  createBridge(session, adapter, adapterId).connect();
17963
19102
  }
17964
19103
  }
17965
- log10.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
19104
+ log11.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
17966
19105
  } catch (rollbackErr) {
17967
19106
  session.fail(`Switch failed and rollback failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`);
17968
- log10.error({ sessionId, fromAgent, toAgent, err, rollbackErr }, "Agent switch failed and rollback also failed");
19107
+ log11.error({ sessionId, fromAgent, toAgent, err, rollbackErr }, "Agent switch failed and rollback also failed");
17969
19108
  }
17970
19109
  throw err;
17971
19110
  }
@@ -17975,7 +19114,7 @@ var AgentSwitchHandler = class {
17975
19114
  if (adapter) {
17976
19115
  createBridge(session, adapter, adapterId).connect();
17977
19116
  } else {
17978
- log10.warn({ sessionId, adapterId }, "Adapter not available during switch reconnect, skipping bridge");
19117
+ log11.warn({ sessionId, adapterId }, "Adapter not available during switch reconnect, skipping bridge");
17979
19118
  }
17980
19119
  }
17981
19120
  }
@@ -18003,7 +19142,7 @@ init_agent_dependencies();
18003
19142
  init_log();
18004
19143
  import * as fs12 from "fs";
18005
19144
  import * as path11 from "path";
18006
- var log12 = createChildLogger({ module: "agent-catalog" });
19145
+ var log13 = createChildLogger({ module: "agent-catalog" });
18007
19146
  var REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
18008
19147
  var DEFAULT_TTL_HOURS = 24;
18009
19148
  var AgentCatalog = class {
@@ -18037,7 +19176,7 @@ var AgentCatalog = class {
18037
19176
  /** Fetch the latest agent registry from the CDN and update the local cache. */
18038
19177
  async fetchRegistry() {
18039
19178
  try {
18040
- log12.info("Fetching agent registry from CDN...");
19179
+ log13.info("Fetching agent registry from CDN...");
18041
19180
  const response = await this.scopedFetch(this.registryUrl);
18042
19181
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
18043
19182
  const data = await response.json();
@@ -18050,9 +19189,9 @@ var AgentCatalog = class {
18050
19189
  fs12.mkdirSync(path11.dirname(this.cachePath), { recursive: true });
18051
19190
  fs12.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2), { mode: 384 });
18052
19191
  this.enrichInstalledFromRegistry();
18053
- log12.info({ count: this.registryAgents.length }, "Registry updated");
19192
+ log13.info({ count: this.registryAgents.length }, "Registry updated");
18054
19193
  } catch (err) {
18055
- log12.warn({ err }, "Failed to fetch registry, using cached data");
19194
+ log13.warn({ err }, "Failed to fetch registry, using cached data");
18056
19195
  }
18057
19196
  }
18058
19197
  /** Re-fetch registry only if the local cache has expired (24-hour TTL). */
@@ -18260,7 +19399,7 @@ var AgentCatalog = class {
18260
19399
  }
18261
19400
  }
18262
19401
  if (changed) {
18263
- log12.info("Enriched installed agents with registry data");
19402
+ log13.info("Enriched installed agents with registry data");
18264
19403
  }
18265
19404
  }
18266
19405
  isCacheStale() {
@@ -18280,11 +19419,11 @@ var AgentCatalog = class {
18280
19419
  const raw = JSON.parse(fs12.readFileSync(this.cachePath, "utf-8"));
18281
19420
  if (raw.data?.agents) {
18282
19421
  this.registryAgents = raw.data.agents;
18283
- log12.debug({ count: this.registryAgents.length }, "Loaded registry from cache");
19422
+ log13.debug({ count: this.registryAgents.length }, "Loaded registry from cache");
18284
19423
  return;
18285
19424
  }
18286
19425
  } catch {
18287
- log12.warn("Failed to load registry cache");
19426
+ log13.warn("Failed to load registry cache");
18288
19427
  }
18289
19428
  }
18290
19429
  try {
@@ -18297,13 +19436,13 @@ var AgentCatalog = class {
18297
19436
  if (fs12.existsSync(candidate)) {
18298
19437
  const raw = JSON.parse(fs12.readFileSync(candidate, "utf-8"));
18299
19438
  this.registryAgents = raw.agents ?? [];
18300
- log12.debug({ count: this.registryAgents.length }, "Loaded registry from bundled snapshot");
19439
+ log13.debug({ count: this.registryAgents.length }, "Loaded registry from bundled snapshot");
18301
19440
  return;
18302
19441
  }
18303
19442
  }
18304
- log12.warn("No registry data available (no cache, no snapshot)");
19443
+ log13.warn("No registry data available (no cache, no snapshot)");
18305
19444
  } catch {
18306
- log12.warn("Failed to load bundled registry snapshot");
19445
+ log13.warn("Failed to load bundled registry snapshot");
18307
19446
  }
18308
19447
  }
18309
19448
  };
@@ -18332,7 +19471,7 @@ init_log();
18332
19471
  import * as fs13 from "fs";
18333
19472
  import * as path12 from "path";
18334
19473
  import { z as z2 } from "zod";
18335
- var log13 = createChildLogger({ module: "agent-store" });
19474
+ var log14 = createChildLogger({ module: "agent-store" });
18336
19475
  var InstalledAgentSchema = z2.object({
18337
19476
  registryId: z2.string().nullable(),
18338
19477
  name: z2.string(),
@@ -18368,11 +19507,11 @@ var AgentStore = class {
18368
19507
  if (result.success) {
18369
19508
  this.data = result.data;
18370
19509
  } else {
18371
- log13.warn({ errors: result.error.issues }, "Invalid agents.json, starting fresh");
19510
+ log14.warn({ errors: result.error.issues }, "Invalid agents.json, starting fresh");
18372
19511
  this.data = { version: 1, installed: {} };
18373
19512
  }
18374
19513
  } catch (err) {
18375
- log13.warn({ err }, "Failed to read agents.json, starting fresh");
19514
+ log14.warn({ err }, "Failed to read agents.json, starting fresh");
18376
19515
  this.data = { version: 1, installed: {} };
18377
19516
  }
18378
19517
  }
@@ -18827,7 +19966,7 @@ function createPluginContext(opts) {
18827
19966
  }
18828
19967
  };
18829
19968
  const baseLog = opts.log ?? noopLog;
18830
- const log38 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
19969
+ const log39 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
18831
19970
  const storageImpl = new PluginStorageImpl(storagePath);
18832
19971
  const storage = {
18833
19972
  async get(key) {
@@ -18897,7 +20036,7 @@ function createPluginContext(opts) {
18897
20036
  const ctx = {
18898
20037
  pluginName,
18899
20038
  pluginConfig,
18900
- log: log38,
20039
+ log: log39,
18901
20040
  storage,
18902
20041
  on(event, handler) {
18903
20042
  requirePermission(permissions, "events:read", "on()");
@@ -18932,7 +20071,7 @@ function createPluginContext(opts) {
18932
20071
  const registry = serviceRegistry.get("command-registry");
18933
20072
  if (registry && typeof registry.register === "function") {
18934
20073
  registry.register(def, pluginName);
18935
- log38.debug(`Command '/${def.name}' registered`);
20074
+ log39.debug(`Command '/${def.name}' registered`);
18936
20075
  }
18937
20076
  },
18938
20077
  async sendMessage(_sessionId, _content) {
@@ -18995,7 +20134,7 @@ function createPluginContext(opts) {
18995
20134
  const registry = serviceRegistry.get("field-registry");
18996
20135
  if (registry && typeof registry.register === "function") {
18997
20136
  registry.register(pluginName, fields);
18998
- log38.debug(`Registered ${fields.length} editable field(s) for ${pluginName}`);
20137
+ log39.debug(`Registered ${fields.length} editable field(s) for ${pluginName}`);
18999
20138
  }
19000
20139
  },
19001
20140
  get sessions() {
@@ -19335,7 +20474,7 @@ var LifecycleManager = class {
19335
20474
 
19336
20475
  // src/core/menu-registry.ts
19337
20476
  init_log();
19338
- var log14 = createChildLogger({ module: "menu-registry" });
20477
+ var log15 = createChildLogger({ module: "menu-registry" });
19339
20478
  var MenuRegistry = class {
19340
20479
  items = /* @__PURE__ */ new Map();
19341
20480
  /** Register or replace a menu item by its unique ID. */
@@ -19357,7 +20496,7 @@ var MenuRegistry = class {
19357
20496
  try {
19358
20497
  return item.visible();
19359
20498
  } catch (err) {
19360
- log14.warn({ err, id: item.id }, "MenuItem visible() threw, hiding item");
20499
+ log15.warn({ err, id: item.id }, "MenuItem visible() threw, hiding item");
19361
20500
  return false;
19362
20501
  }
19363
20502
  }).sort((a, b) => a.priority - b.priority);
@@ -19425,7 +20564,7 @@ openacp api new claude ~/project
19425
20564
  }
19426
20565
 
19427
20566
  // src/core/assistant/assistant-registry.ts
19428
- var log15 = createChildLogger({ module: "assistant-registry" });
20567
+ var log16 = createChildLogger({ module: "assistant-registry" });
19429
20568
  var AssistantRegistry = class {
19430
20569
  sections = /* @__PURE__ */ new Map();
19431
20570
  _instanceRoot = "";
@@ -19436,7 +20575,7 @@ var AssistantRegistry = class {
19436
20575
  /** Register a prompt section. Overwrites any existing section with the same id. */
19437
20576
  register(section) {
19438
20577
  if (this.sections.has(section.id)) {
19439
- log15.warn({ id: section.id }, "Assistant section overwritten");
20578
+ log16.warn({ id: section.id }, "Assistant section overwritten");
19440
20579
  }
19441
20580
  this.sections.set(section.id, section);
19442
20581
  }
@@ -19472,7 +20611,7 @@ ${context}`);
19472
20611
  parts.push("```bash\n" + cmds + "\n```");
19473
20612
  }
19474
20613
  } catch (err) {
19475
- log15.warn({ err, sectionId: section.id }, "Assistant section buildContext() failed, skipping");
20614
+ log16.warn({ err, sectionId: section.id }, "Assistant section buildContext() failed, skipping");
19476
20615
  }
19477
20616
  }
19478
20617
  parts.push(buildAssistantGuidelines(this._instanceRoot));
@@ -19482,7 +20621,7 @@ ${context}`);
19482
20621
 
19483
20622
  // src/core/assistant/assistant-manager.ts
19484
20623
  init_log();
19485
- var log16 = createChildLogger({ module: "assistant-manager" });
20624
+ var log17 = createChildLogger({ module: "assistant-manager" });
19486
20625
  var AssistantManager = class {
19487
20626
  constructor(core, registry) {
19488
20627
  this.core = core;
@@ -19513,7 +20652,7 @@ var AssistantManager = class {
19513
20652
  this.sessions.set(channelId, session);
19514
20653
  const systemPrompt = this.registry.buildSystemPrompt(channelId);
19515
20654
  this.pendingSystemPrompts.set(channelId, systemPrompt);
19516
- log16.info(
20655
+ log17.info(
19517
20656
  { sessionId: session.id, channelId, reused: !!existing },
19518
20657
  existing ? "Assistant session reused (system prompt deferred)" : "Assistant spawned (system prompt deferred)"
19519
20658
  );
@@ -19704,6 +20843,15 @@ function registerCoreMenuItems(registry) {
19704
20843
  group: "config",
19705
20844
  action: { type: "command", command: "/integrate" }
19706
20845
  });
20846
+ registry.register({
20847
+ id: "core:proxy",
20848
+ label: "\u{1F310} Proxy Routing",
20849
+ priority: 32,
20850
+ group: "config",
20851
+ action: { type: "command", command: "/proxy" },
20852
+ // Keep already-sent m:core:proxy buttons valid while nesting new entry points under Settings.
20853
+ visible: () => false
20854
+ });
19707
20855
  registry.register({
19708
20856
  id: "core:restart",
19709
20857
  label: "\u{1F504} Restart",
@@ -19739,7 +20887,7 @@ init_log();
19739
20887
  init_native_stt();
19740
20888
  init_events();
19741
20889
  init_proxy_service();
19742
- var log17 = createChildLogger({ module: "core" });
20890
+ var log18 = createChildLogger({ module: "core" });
19743
20891
  var OpenACPCore = class {
19744
20892
  configManager;
19745
20893
  agentCatalog;
@@ -19822,7 +20970,7 @@ var OpenACPCore = class {
19822
20970
  this.proxyService.onRouteChanged(async (scope) => {
19823
20971
  if (scope === "global" || scope === "agents.default" || scope.startsWith("agents.")) {
19824
20972
  await this.agentManager.destroyWarm();
19825
- log17.info({ scope }, "Proxy route changed; idle agent warm pool invalidated");
20973
+ log18.info({ scope }, "Proxy route changed; idle agent warm pool invalidated");
19826
20974
  }
19827
20975
  });
19828
20976
  const storePath = ctx.paths.sessions;
@@ -19884,7 +21032,7 @@ var OpenACPCore = class {
19884
21032
  if (configPath === "logging.level" && typeof value === "string") {
19885
21033
  const { setLogLevel: setLogLevel2 } = await Promise.resolve().then(() => (init_log(), log_exports));
19886
21034
  setLogLevel2(value);
19887
- log17.info({ level: value }, "Log level changed at runtime");
21035
+ log18.info({ level: value }, "Log level changed at runtime");
19888
21036
  }
19889
21037
  if (configPath.startsWith("speech.")) {
19890
21038
  const speechSvc = this.lifecycleManager.serviceRegistry.get("speech");
@@ -19894,7 +21042,7 @@ var OpenACPCore = class {
19894
21042
  const pluginCfg = await settingsMgr.loadSettings("@openacp/speech");
19895
21043
  const newSpeechConfig = buildSpeechServiceConfig(pluginCfg);
19896
21044
  speechSvc.refreshProviders(newSpeechConfig);
19897
- log17.info("Speech service config updated at runtime (from plugin settings)");
21045
+ log18.info("Speech service config updated at runtime (from plugin settings)");
19898
21046
  }
19899
21047
  }
19900
21048
  }
@@ -19935,14 +21083,14 @@ var OpenACPCore = class {
19935
21083
  */
19936
21084
  async start() {
19937
21085
  this.agentCatalog.refreshRegistryIfStale().catch((err) => {
19938
- log17.warn({ err }, "Background registry refresh failed");
21086
+ log18.warn({ err }, "Background registry refresh failed");
19939
21087
  });
19940
21088
  const failures = [];
19941
21089
  for (const [name, adapter] of this.adapters.entries()) {
19942
21090
  try {
19943
21091
  await adapter.start();
19944
21092
  } catch (err) {
19945
- log17.error({ err, adapter: name }, `Adapter "${name}" failed to start`);
21093
+ log18.error({ err, adapter: name }, `Adapter "${name}" failed to start`);
19946
21094
  failures.push({ name, error: err });
19947
21095
  }
19948
21096
  }
@@ -20023,7 +21171,7 @@ var OpenACPCore = class {
20023
21171
  * If no session is found, the user is told to start one with /new.
20024
21172
  */
20025
21173
  async handleMessage(message, initialMeta) {
20026
- log17.debug(
21174
+ log18.debug(
20027
21175
  {
20028
21176
  channelId: message.channelId,
20029
21177
  threadId: message.threadId,
@@ -20044,7 +21192,7 @@ var OpenACPCore = class {
20044
21192
  }
20045
21193
  const access2 = await this.securityGuard.checkAccess(message);
20046
21194
  if (!access2.allowed) {
20047
- log17.warn({ userId: message.userId, reason: access2.reason }, "Access denied");
21195
+ log18.warn({ userId: message.userId, reason: access2.reason }, "Access denied");
20048
21196
  if (access2.reason.includes("Session limit")) {
20049
21197
  const adapter = this.adapters.get(message.channelId);
20050
21198
  if (adapter) {
@@ -20058,7 +21206,7 @@ var OpenACPCore = class {
20058
21206
  }
20059
21207
  let session = await this.sessionFactory.getOrResume(message.channelId, message.threadId);
20060
21208
  if (!session) {
20061
- log17.warn(
21209
+ log18.warn(
20062
21210
  { channelId: message.channelId, threadId: message.threadId },
20063
21211
  "No session found for thread (in-memory miss + lazy resume returned null)"
20064
21212
  );
@@ -20110,7 +21258,7 @@ ${text3}`;
20110
21258
  });
20111
21259
  session.enqueuePrompt(text3, attachments, routing, turnId, meta).catch((err) => {
20112
21260
  const reason = err instanceof Error ? err.message : String(err);
20113
- log17.warn({ err, sessionId: session.id, turnId, reason }, "enqueuePrompt failed \u2014 emitting message:failed");
21261
+ log18.warn({ err, sessionId: session.id, turnId, reason }, "enqueuePrompt failed \u2014 emitting message:failed");
20114
21262
  this.eventBus.emit(BusEvent.MESSAGE_FAILED, {
20115
21263
  sessionId: session.id,
20116
21264
  turnId,
@@ -20227,7 +21375,7 @@ ${text3}`;
20227
21375
  const bridge = this.createBridge(session, adapter, session.channelId);
20228
21376
  bridge.connect();
20229
21377
  adapter.flushPendingSkillCommands?.(session.id).catch((err) => {
20230
- log17.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
21378
+ log18.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
20231
21379
  });
20232
21380
  if (params.createThread && session.threadId) {
20233
21381
  this.eventBus.emit(BusEvent.SESSION_THREAD_READY, {
@@ -20241,14 +21389,14 @@ ${text3}`;
20241
21389
  session.agentInstance.onPermissionRequest = async (permRequest) => {
20242
21390
  const allowOption = permRequest.options.find((o) => o.isAllow);
20243
21391
  if (!allowOption) {
20244
- log17.warn(
21392
+ log18.warn(
20245
21393
  { sessionId: session.id, permissionId: permRequest.id, description: permRequest.description },
20246
21394
  "Headless session has no allow option for permission request \u2014 skipping auto-approve, will time out"
20247
21395
  );
20248
21396
  return new Promise(() => {
20249
21397
  });
20250
21398
  }
20251
- log17.warn(
21399
+ log18.warn(
20252
21400
  { sessionId: session.id, permissionId: permRequest.id, option: allowOption.id },
20253
21401
  `Auto-approving permission "${permRequest.description}" for headless session \u2014 no adapter connected`
20254
21402
  );
@@ -20302,7 +21450,7 @@ ${text3}`;
20302
21450
  notificationManager: this.notificationManager,
20303
21451
  tunnelService: this._tunnelService
20304
21452
  });
20305
- log17.info(
21453
+ log18.info(
20306
21454
  { sessionId: session.id, agentName: params.agentName },
20307
21455
  "Session created via pipeline"
20308
21456
  );
@@ -20721,33 +21869,33 @@ init_doctor();
20721
21869
  init_config_registry();
20722
21870
 
20723
21871
  // src/core/config/config-editor.ts
20724
- import * as path32 from "path";
21872
+ import * as path33 from "path";
20725
21873
  import * as clack2 from "@clack/prompts";
20726
21874
 
20727
21875
  // src/cli/autostart.ts
20728
21876
  init_log();
20729
21877
  import { execFileSync as execFileSync6 } from "child_process";
20730
- import * as fs27 from "fs";
20731
- import * as path25 from "path";
20732
- import * as os7 from "os";
20733
- var log19 = createChildLogger({ module: "autostart" });
20734
- var LEGACY_LAUNCHD_PLIST_PATH = path25.join(os7.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
20735
- var LEGACY_SYSTEMD_SERVICE_PATH = path25.join(os7.homedir(), ".config", "systemd", "user", "openacp.service");
21878
+ import * as fs29 from "fs";
21879
+ import * as path27 from "path";
21880
+ import * as os9 from "os";
21881
+ var log20 = createChildLogger({ module: "autostart" });
21882
+ var LEGACY_LAUNCHD_PLIST_PATH = path27.join(os9.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
21883
+ var LEGACY_SYSTEMD_SERVICE_PATH = path27.join(os9.homedir(), ".config", "systemd", "user", "openacp.service");
20736
21884
  function getLaunchdLabel(instanceId) {
20737
21885
  return `com.openacp.daemon.${instanceId}`;
20738
21886
  }
20739
21887
  function getLaunchdPlistPath(instanceId) {
20740
- return path25.join(os7.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
21888
+ return path27.join(os9.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
20741
21889
  }
20742
21890
  function getSystemdServiceName(instanceId) {
20743
21891
  return `openacp-${instanceId}`;
20744
21892
  }
20745
21893
  function getSystemdServicePath(instanceId) {
20746
- return path25.join(os7.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
21894
+ return path27.join(os9.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
20747
21895
  }
20748
21896
  function getAutoStartState(instanceId) {
20749
21897
  if (process.platform === "linux") {
20750
- const installed = fs27.existsSync(getSystemdServicePath(instanceId));
21898
+ const installed = fs29.existsSync(getSystemdServicePath(instanceId));
20751
21899
  if (!installed) return { installed: false, manager: null, active: false };
20752
21900
  try {
20753
21901
  const output = execFileSync6("systemctl", [
@@ -20769,7 +21917,7 @@ function getAutoStartState(instanceId) {
20769
21917
  }
20770
21918
  }
20771
21919
  if (process.platform === "darwin") {
20772
- const installed = fs27.existsSync(getLaunchdPlistPath(instanceId));
21920
+ const installed = fs29.existsSync(getLaunchdPlistPath(instanceId));
20773
21921
  if (!installed) return { installed: false, manager: null, active: false };
20774
21922
  try {
20775
21923
  const uid = process.getuid();
@@ -20799,7 +21947,7 @@ function escapeSystemdValue(str) {
20799
21947
  }
20800
21948
  function generateLaunchdPlist(nodePath, cliPath, logDir2, instanceRoot, instanceId) {
20801
21949
  const label = getLaunchdLabel(instanceId);
20802
- const logFile = path25.join(logDir2, "openacp.log");
21950
+ const logFile = path27.join(logDir2, "openacp.log");
20803
21951
  return `<?xml version="1.0" encoding="UTF-8"?>
20804
21952
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
20805
21953
  <plist version="1.0">
@@ -20851,32 +21999,32 @@ WantedBy=default.target
20851
21999
  `;
20852
22000
  }
20853
22001
  function migrateLegacy() {
20854
- if (process.platform === "darwin" && fs27.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
22002
+ if (process.platform === "darwin" && fs29.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
20855
22003
  try {
20856
22004
  const uid = process.getuid();
20857
22005
  execFileSync6("launchctl", ["bootout", `gui/${uid}`, "com.openacp.daemon"], { stdio: "pipe" });
20858
22006
  } catch {
20859
22007
  }
20860
22008
  try {
20861
- fs27.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
22009
+ fs29.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
20862
22010
  } catch {
20863
22011
  }
20864
- log19.info("Removed legacy single-instance LaunchAgent");
22012
+ log20.info("Removed legacy single-instance LaunchAgent");
20865
22013
  }
20866
- if (process.platform === "linux" && fs27.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
22014
+ if (process.platform === "linux" && fs29.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
20867
22015
  try {
20868
22016
  execFileSync6("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
20869
22017
  } catch {
20870
22018
  }
20871
22019
  try {
20872
- fs27.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
22020
+ fs29.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
20873
22021
  } catch {
20874
22022
  }
20875
22023
  try {
20876
22024
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20877
22025
  } catch {
20878
22026
  }
20879
- log19.info("Removed legacy single-instance systemd service");
22027
+ log20.info("Removed legacy single-instance systemd service");
20880
22028
  }
20881
22029
  }
20882
22030
  function installAutoStart(logDir2, instanceRoot, instanceId) {
@@ -20884,26 +22032,26 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
20884
22032
  return { success: false, error: "Auto-start not supported on this platform" };
20885
22033
  }
20886
22034
  const nodePath = process.execPath;
20887
- const cliPath = path25.resolve(process.argv[1]);
20888
- const resolvedLogDir = logDir2.startsWith("~") ? path25.join(os7.homedir(), logDir2.slice(1)) : logDir2;
22035
+ const cliPath = path27.resolve(process.argv[1]);
22036
+ const resolvedLogDir = logDir2.startsWith("~") ? path27.join(os9.homedir(), logDir2.slice(1)) : logDir2;
20889
22037
  try {
20890
22038
  migrateLegacy();
20891
22039
  if (process.platform === "darwin") {
20892
22040
  const plistPath = getLaunchdPlistPath(instanceId);
20893
22041
  const plist = generateLaunchdPlist(nodePath, cliPath, resolvedLogDir, instanceRoot, instanceId);
20894
- const dir = path25.dirname(plistPath);
20895
- fs27.mkdirSync(dir, { recursive: true });
22042
+ const dir = path27.dirname(plistPath);
22043
+ fs29.mkdirSync(dir, { recursive: true });
20896
22044
  const uid = process.getuid();
20897
22045
  const domain = `gui/${uid}`;
20898
- const previous = fs27.existsSync(plistPath) ? fs27.readFileSync(plistPath, "utf8") : void 0;
22046
+ const previous = fs29.existsSync(plistPath) ? fs29.readFileSync(plistPath, "utf8") : void 0;
20899
22047
  const previousState = getAutoStartState(instanceId);
20900
22048
  const tmp = `${plistPath}.${process.pid}.tmp`;
20901
22049
  let replaced = false;
20902
22050
  let bootedOut = false;
20903
22051
  try {
20904
- fs27.writeFileSync(tmp, plist, { mode: 384 });
22052
+ fs29.writeFileSync(tmp, plist, { mode: 384 });
20905
22053
  execFileSync6("plutil", ["-lint", tmp], { stdio: "pipe" });
20906
- fs27.renameSync(tmp, plistPath);
22054
+ fs29.renameSync(tmp, plistPath);
20907
22055
  replaced = true;
20908
22056
  if (previousState.active) {
20909
22057
  execFileSync6("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
@@ -20912,7 +22060,7 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
20912
22060
  execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
20913
22061
  } catch (error) {
20914
22062
  try {
20915
- fs27.rmSync(tmp, { force: true });
22063
+ fs29.rmSync(tmp, { force: true });
20916
22064
  } catch {
20917
22065
  }
20918
22066
  if (replaced) {
@@ -20922,48 +22070,48 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
20922
22070
  rollbackBootedOut = true;
20923
22071
  } catch {
20924
22072
  }
20925
- if (previous === void 0) fs27.rmSync(plistPath, { force: true });
22073
+ if (previous === void 0) fs29.rmSync(plistPath, { force: true });
20926
22074
  else {
20927
22075
  const rollbackTmp = `${plistPath}.${process.pid}.rollback.tmp`;
20928
- fs27.writeFileSync(rollbackTmp, previous, { mode: 384 });
20929
- fs27.renameSync(rollbackTmp, plistPath);
22076
+ fs29.writeFileSync(rollbackTmp, previous, { mode: 384 });
22077
+ fs29.renameSync(rollbackTmp, plistPath);
20930
22078
  if (previousState.active || bootedOut || rollbackBootedOut) execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
20931
22079
  }
20932
22080
  }
20933
22081
  throw error;
20934
22082
  }
20935
- log19.info({ instanceId }, "LaunchAgent installed");
22083
+ log20.info({ instanceId }, "LaunchAgent installed");
20936
22084
  return { success: true };
20937
22085
  }
20938
22086
  if (process.platform === "linux") {
20939
22087
  const servicePath = getSystemdServicePath(instanceId);
20940
22088
  const serviceName = getSystemdServiceName(instanceId);
20941
22089
  const unit = generateSystemdUnit(nodePath, cliPath, instanceRoot, instanceId);
20942
- const dir = path25.dirname(servicePath);
20943
- fs27.mkdirSync(dir, { recursive: true });
20944
- const previous = fs27.existsSync(servicePath) ? fs27.readFileSync(servicePath, "utf8") : void 0;
22090
+ const dir = path27.dirname(servicePath);
22091
+ fs29.mkdirSync(dir, { recursive: true });
22092
+ const previous = fs29.existsSync(servicePath) ? fs29.readFileSync(servicePath, "utf8") : void 0;
20945
22093
  const tmp = `${servicePath}.${process.pid}.tmp`;
20946
- fs27.writeFileSync(tmp, unit, { mode: 384 });
20947
- fs27.renameSync(tmp, servicePath);
22094
+ fs29.writeFileSync(tmp, unit, { mode: 384 });
22095
+ fs29.renameSync(tmp, servicePath);
20948
22096
  try {
20949
22097
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20950
22098
  execFileSync6("systemctl", ["--user", "enable", serviceName], { stdio: "pipe" });
20951
22099
  } catch (error) {
20952
- if (previous === void 0) fs27.unlinkSync(servicePath);
20953
- else fs27.writeFileSync(servicePath, previous);
22100
+ if (previous === void 0) fs29.unlinkSync(servicePath);
22101
+ else fs29.writeFileSync(servicePath, previous);
20954
22102
  try {
20955
22103
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20956
22104
  } catch {
20957
22105
  }
20958
22106
  throw error;
20959
22107
  }
20960
- log19.info({ instanceId }, "systemd user service installed");
22108
+ log20.info({ instanceId }, "systemd user service installed");
20961
22109
  return { success: true };
20962
22110
  }
20963
22111
  return { success: false, error: "Unsupported platform" };
20964
22112
  } catch (e) {
20965
22113
  const msg = e.message;
20966
- log19.error({ err: msg }, "Failed to install auto-start");
22114
+ log20.error({ err: msg }, "Failed to install auto-start");
20967
22115
  return { success: false, error: msg };
20968
22116
  }
20969
22117
  }
@@ -20974,44 +22122,44 @@ function uninstallAutoStart(instanceId) {
20974
22122
  try {
20975
22123
  if (process.platform === "darwin") {
20976
22124
  const plistPath = getLaunchdPlistPath(instanceId);
20977
- if (fs27.existsSync(plistPath)) {
22125
+ if (fs29.existsSync(plistPath)) {
20978
22126
  const uid = process.getuid();
20979
22127
  try {
20980
22128
  execFileSync6("launchctl", ["bootout", `gui/${uid}`, plistPath], { stdio: "pipe" });
20981
22129
  } catch {
20982
22130
  }
20983
- fs27.unlinkSync(plistPath);
20984
- log19.info({ instanceId }, "LaunchAgent removed");
22131
+ fs29.unlinkSync(plistPath);
22132
+ log20.info({ instanceId }, "LaunchAgent removed");
20985
22133
  }
20986
22134
  return { success: true };
20987
22135
  }
20988
22136
  if (process.platform === "linux") {
20989
22137
  const servicePath = getSystemdServicePath(instanceId);
20990
22138
  const serviceName = getSystemdServiceName(instanceId);
20991
- if (fs27.existsSync(servicePath)) {
22139
+ if (fs29.existsSync(servicePath)) {
20992
22140
  try {
20993
22141
  execFileSync6("systemctl", ["--user", "disable", serviceName], { stdio: "pipe" });
20994
22142
  } catch {
20995
22143
  }
20996
- fs27.unlinkSync(servicePath);
22144
+ fs29.unlinkSync(servicePath);
20997
22145
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20998
- log19.info({ instanceId }, "systemd user service removed");
22146
+ log20.info({ instanceId }, "systemd user service removed");
20999
22147
  }
21000
22148
  return { success: true };
21001
22149
  }
21002
22150
  return { success: false, error: "Unsupported platform" };
21003
22151
  } catch (e) {
21004
22152
  const msg = e.message;
21005
- log19.error({ err: msg }, "Failed to uninstall auto-start");
22153
+ log20.error({ err: msg }, "Failed to uninstall auto-start");
21006
22154
  return { success: false, error: msg };
21007
22155
  }
21008
22156
  }
21009
22157
  function isAutoStartInstalled(instanceId) {
21010
22158
  if (process.platform === "darwin") {
21011
- return fs27.existsSync(getLaunchdPlistPath(instanceId));
22159
+ return fs29.existsSync(getLaunchdPlistPath(instanceId));
21012
22160
  }
21013
22161
  if (process.platform === "linux") {
21014
- return fs27.existsSync(getSystemdServicePath(instanceId));
22162
+ return fs29.existsSync(getSystemdServicePath(instanceId));
21015
22163
  }
21016
22164
  return false;
21017
22165
  }
@@ -21020,25 +22168,25 @@ function isAutoStartInstalled(instanceId) {
21020
22168
  init_instance_context();
21021
22169
  init_instance_registry();
21022
22170
  init_log();
21023
- import fs30 from "fs";
21024
- import path28 from "path";
21025
- var log20 = createChildLogger({ module: "resolve-instance-id" });
22171
+ import fs31 from "fs";
22172
+ import path29 from "path";
22173
+ var log21 = createChildLogger({ module: "resolve-instance-id" });
21026
22174
  function resolveInstanceId(instanceRoot) {
21027
22175
  try {
21028
- const configPath = path28.join(instanceRoot, "config.json");
21029
- const raw = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
22176
+ const configPath = path29.join(instanceRoot, "config.json");
22177
+ const raw = JSON.parse(fs31.readFileSync(configPath, "utf-8"));
21030
22178
  if (raw.id && typeof raw.id === "string") return raw.id;
21031
22179
  } catch {
21032
22180
  }
21033
22181
  try {
21034
- const reg = new InstanceRegistry(path28.join(getGlobalRoot(), "instances.json"));
22182
+ const reg = new InstanceRegistry(path29.join(getGlobalRoot(), "instances.json"));
21035
22183
  reg.load();
21036
22184
  const entry = reg.getByRoot(instanceRoot);
21037
22185
  if (entry?.id) return entry.id;
21038
22186
  } catch (err) {
21039
- log20.debug({ err: err.message, instanceRoot }, "Could not read instance registry, using fallback id");
22187
+ log21.debug({ err: err.message, instanceRoot }, "Could not read instance registry, using fallback id");
21040
22188
  }
21041
- return path28.basename(path28.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
22189
+ return path29.basename(path29.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
21042
22190
  }
21043
22191
 
21044
22192
  // src/core/config/config-editor.ts
@@ -21610,7 +22758,7 @@ async function runConfigEditor(configManager, mode = "file", apiPort, settingsMa
21610
22758
  await configManager.load();
21611
22759
  const config = configManager.get();
21612
22760
  const updates = {};
21613
- const instanceRoot = path32.dirname(configManager.getConfigPath());
22761
+ const instanceRoot = path33.dirname(configManager.getConfigPath());
21614
22762
  console.log(`
21615
22763
  ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
21616
22764
  console.log(dim(`Config: ${configManager.getConfigPath()}`));
@@ -21667,17 +22815,17 @@ ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
21667
22815
  async function sendConfigViaApi(port, updates) {
21668
22816
  const { apiCall: call } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
21669
22817
  const paths = flattenToPaths(updates);
21670
- for (const { path: path37, value } of paths) {
22818
+ for (const { path: path38, value } of paths) {
21671
22819
  const res = await call(port, "/api/config", {
21672
22820
  method: "PATCH",
21673
22821
  headers: { "Content-Type": "application/json" },
21674
- body: JSON.stringify({ path: path37, value })
22822
+ body: JSON.stringify({ path: path38, value })
21675
22823
  });
21676
22824
  const data = await res.json();
21677
22825
  if (!res.ok) {
21678
- console.log(warn(`Failed to update ${path37}: ${data.error}`));
22826
+ console.log(warn(`Failed to update ${path38}: ${data.error}`));
21679
22827
  } else if (data.needsRestart) {
21680
- console.log(warn(`${path37} updated \u2014 restart required`));
22828
+ console.log(warn(`${path38} updated \u2014 restart required`));
21681
22829
  }
21682
22830
  }
21683
22831
  }
@@ -21697,25 +22845,25 @@ function flattenToPaths(obj, prefix = "") {
21697
22845
  // src/cli/daemon.ts
21698
22846
  init_config();
21699
22847
  import { spawn as spawn3 } from "child_process";
21700
- import * as fs33 from "fs";
21701
- import * as path33 from "path";
22848
+ import * as fs34 from "fs";
22849
+ import * as path34 from "path";
21702
22850
  function getPidPath(root) {
21703
- return path33.join(root, "openacp.pid");
22851
+ return path34.join(root, "openacp.pid");
21704
22852
  }
21705
22853
  function getLogDir(root) {
21706
- return path33.join(root, "logs");
22854
+ return path34.join(root, "logs");
21707
22855
  }
21708
22856
  function getRunningMarker(root) {
21709
- return path33.join(root, "running");
22857
+ return path34.join(root, "running");
21710
22858
  }
21711
22859
  function writePidFile(pidPath, pid) {
21712
- const dir = path33.dirname(pidPath);
21713
- fs33.mkdirSync(dir, { recursive: true });
21714
- fs33.writeFileSync(pidPath, String(pid));
22860
+ const dir = path34.dirname(pidPath);
22861
+ fs34.mkdirSync(dir, { recursive: true });
22862
+ fs34.writeFileSync(pidPath, String(pid));
21715
22863
  }
21716
22864
  function readPidFile(pidPath) {
21717
22865
  try {
21718
- const content = fs33.readFileSync(pidPath, "utf-8").trim();
22866
+ const content = fs34.readFileSync(pidPath, "utf-8").trim();
21719
22867
  const pid = parseInt(content, 10);
21720
22868
  return isNaN(pid) ? null : pid;
21721
22869
  } catch {
@@ -21724,7 +22872,7 @@ function readPidFile(pidPath) {
21724
22872
  }
21725
22873
  function removePidFile(pidPath) {
21726
22874
  try {
21727
- fs33.unlinkSync(pidPath);
22875
+ fs34.unlinkSync(pidPath);
21728
22876
  } catch {
21729
22877
  }
21730
22878
  }
@@ -21757,12 +22905,12 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
21757
22905
  return { error: `Already running (PID ${pid})` };
21758
22906
  }
21759
22907
  const resolvedLogDir = logDir2 ? expandHome2(logDir2) : getLogDir(instanceRoot);
21760
- fs33.mkdirSync(resolvedLogDir, { recursive: true });
21761
- const logFile = path33.join(resolvedLogDir, "openacp.log");
21762
- const cliPath = path33.resolve(process.argv[1]);
22908
+ fs34.mkdirSync(resolvedLogDir, { recursive: true });
22909
+ const logFile = path34.join(resolvedLogDir, "openacp.log");
22910
+ const cliPath = path34.resolve(process.argv[1]);
21763
22911
  const nodePath = process.execPath;
21764
- const out = fs33.openSync(logFile, "a");
21765
- const err = fs33.openSync(logFile, "a");
22912
+ const out = fs34.openSync(logFile, "a");
22913
+ const err = fs34.openSync(logFile, "a");
21766
22914
  const child = spawn3(nodePath, [cliPath, "--daemon-child"], {
21767
22915
  detached: true,
21768
22916
  stdio: ["ignore", out, err],
@@ -21771,8 +22919,8 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
21771
22919
  ...instanceRoot ? { OPENACP_INSTANCE_ROOT: instanceRoot } : {}
21772
22920
  }
21773
22921
  });
21774
- fs33.closeSync(out);
21775
- fs33.closeSync(err);
22922
+ fs34.closeSync(out);
22923
+ fs34.closeSync(err);
21776
22924
  if (!child.pid) {
21777
22925
  return { error: "Failed to spawn daemon process" };
21778
22926
  }
@@ -21843,12 +22991,12 @@ async function stopDaemon(pidPath, instanceRoot) {
21843
22991
  }
21844
22992
  function markRunning(root) {
21845
22993
  const marker = getRunningMarker(root);
21846
- fs33.mkdirSync(path33.dirname(marker), { recursive: true });
21847
- fs33.writeFileSync(marker, "");
22994
+ fs34.mkdirSync(path34.dirname(marker), { recursive: true });
22995
+ fs34.writeFileSync(marker, "");
21848
22996
  }
21849
22997
  function clearRunning(root) {
21850
22998
  try {
21851
- fs33.unlinkSync(getRunningMarker(root));
22999
+ fs34.unlinkSync(getRunningMarker(root));
21852
23000
  } catch {
21853
23001
  }
21854
23002
  }
@@ -21864,7 +23012,7 @@ init_static_server();
21864
23012
 
21865
23013
  // src/plugins/telegram/topic-manager.ts
21866
23014
  init_log();
21867
- var log23 = createChildLogger({ module: "topic-manager" });
23015
+ var log24 = createChildLogger({ module: "topic-manager" });
21868
23016
  var TopicManager = class {
21869
23017
  constructor(sessionManager, adapter, systemTopicIds) {
21870
23018
  this.sessionManager = sessionManager;
@@ -21916,7 +23064,7 @@ var TopicManager = class {
21916
23064
  try {
21917
23065
  await this.adapter.deleteSessionThread?.(sessionId);
21918
23066
  } catch (err) {
21919
- log23.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
23067
+ log24.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
21920
23068
  }
21921
23069
  }
21922
23070
  await this.sessionManager.removeRecord(sessionId);
@@ -21943,7 +23091,7 @@ var TopicManager = class {
21943
23091
  try {
21944
23092
  await this.adapter.deleteSessionThread?.(record.sessionId);
21945
23093
  } catch (err) {
21946
- log23.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
23094
+ log24.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
21947
23095
  }
21948
23096
  }
21949
23097
  await this.sessionManager.removeRecord(record.sessionId);