@n1creator/openacp-cli 2026.712.9 → 2026.712.10

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,7 @@ 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" });
9485
10024
  }
9486
10025
  });
9487
10026
 
@@ -9644,7 +10183,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
9644
10183
  } catch {
9645
10184
  }
9646
10185
  } catch (err) {
9647
- log28.error({ err, fieldPath }, "Failed to toggle config");
10186
+ log29.error({ err, fieldPath }, "Failed to toggle config");
9648
10187
  try {
9649
10188
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
9650
10189
  } catch {
@@ -9725,7 +10264,7 @@ Tap to change:`, {
9725
10264
  } catch {
9726
10265
  }
9727
10266
  } catch (err) {
9728
- log28.error({ err, fieldPath }, "Failed to set config");
10267
+ log29.error({ err, fieldPath }, "Failed to set config");
9729
10268
  try {
9730
10269
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
9731
10270
  } catch {
@@ -9783,13 +10322,13 @@ Tap to change:`, {
9783
10322
  }
9784
10323
  });
9785
10324
  }
9786
- var log28;
10325
+ var log29;
9787
10326
  var init_settings = __esm({
9788
10327
  "src/plugins/telegram/commands/settings.ts"() {
9789
10328
  "use strict";
9790
10329
  init_config_registry();
9791
10330
  init_log();
9792
- log28 = createChildLogger({ module: "telegram-settings" });
10331
+ log29 = createChildLogger({ module: "telegram-settings" });
9793
10332
  }
9794
10333
  });
9795
10334
 
@@ -9836,7 +10375,7 @@ async function handleDoctor(ctx, core) {
9836
10375
  reply_markup: keyboard
9837
10376
  });
9838
10377
  } catch (err) {
9839
- log29.error({ err }, "Doctor command failed");
10378
+ log30.error({ err }, "Doctor command failed");
9840
10379
  await ctx.api.editMessageText(
9841
10380
  ctx.chat.id,
9842
10381
  statusMsg.message_id,
@@ -9885,7 +10424,7 @@ function setupDoctorCallbacks(bot, core) {
9885
10424
  }
9886
10425
  }
9887
10426
  } catch (err) {
9888
- log29.error({ err, index }, "Doctor fix callback failed");
10427
+ log30.error({ err, index }, "Doctor fix callback failed");
9889
10428
  }
9890
10429
  });
9891
10430
  bot.callbackQuery("m:doctor", async (ctx) => {
@@ -9896,13 +10435,13 @@ function setupDoctorCallbacks(bot, core) {
9896
10435
  await handleDoctor(ctx, core);
9897
10436
  });
9898
10437
  }
9899
- var log29, pendingFixesStore;
10438
+ var log30, pendingFixesStore;
9900
10439
  var init_doctor2 = __esm({
9901
10440
  "src/plugins/telegram/commands/doctor.ts"() {
9902
10441
  "use strict";
9903
10442
  init_doctor();
9904
10443
  init_log();
9905
- log29 = createChildLogger({ module: "telegram-cmd-doctor" });
10444
+ log30 = createChildLogger({ module: "telegram-cmd-doctor" });
9906
10445
  pendingFixesStore = /* @__PURE__ */ new Map();
9907
10446
  }
9908
10447
  });
@@ -9961,13 +10500,13 @@ function setupTunnelCallbacks(bot, core) {
9961
10500
  }
9962
10501
  });
9963
10502
  }
9964
- var log30;
10503
+ var log31;
9965
10504
  var init_tunnel2 = __esm({
9966
10505
  "src/plugins/telegram/commands/tunnel.ts"() {
9967
10506
  "use strict";
9968
10507
  init_formatting();
9969
10508
  init_log();
9970
- log30 = createChildLogger({ module: "telegram-cmd-tunnel" });
10509
+ log31 = createChildLogger({ module: "telegram-cmd-tunnel" });
9971
10510
  }
9972
10511
  });
9973
10512
 
@@ -9981,10 +10520,10 @@ async function executeSwitchAgent(ctx, core, sessionId, agentName) {
9981
10520
  `Switched to <b>${escapeHtml(agentName)}</b> (${status})`,
9982
10521
  { parse_mode: "HTML" }
9983
10522
  );
9984
- log31.info({ sessionId, agentName, resumed }, "Agent switched via /switch");
10523
+ log32.info({ sessionId, agentName, resumed }, "Agent switched via /switch");
9985
10524
  } catch (err) {
9986
10525
  await ctx.reply(`Failed to switch agent: ${escapeHtml(String(err.message || err))}`);
9987
- log31.warn({ sessionId, agentName, err: err.message }, "Agent switch failed");
10526
+ log32.warn({ sessionId, agentName, err: err.message }, "Agent switch failed");
9988
10527
  }
9989
10528
  }
9990
10529
  function setupSwitchCallbacks(bot, core) {
@@ -10029,13 +10568,13 @@ Switch to <b>${escapeHtml(agentName)}</b> anyway?`,
10029
10568
  await executeSwitchAgent(ctx, core, session.id, data);
10030
10569
  });
10031
10570
  }
10032
- var log31;
10571
+ var log32;
10033
10572
  var init_switch = __esm({
10034
10573
  "src/plugins/telegram/commands/switch.ts"() {
10035
10574
  "use strict";
10036
10575
  init_formatting();
10037
10576
  init_log();
10038
- log31 = createChildLogger({ module: "telegram-cmd-switch" });
10577
+ log32 = createChildLogger({ module: "telegram-cmd-switch" });
10039
10578
  }
10040
10579
  });
10041
10580
 
@@ -10258,7 +10797,7 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
10258
10797
  if (!menuRegistry) return;
10259
10798
  const item = menuRegistry.getItem(itemId);
10260
10799
  if (!item) {
10261
- log32.warn({ itemId }, "Menu item not found in registry");
10800
+ log33.warn({ itemId }, "Menu item not found in registry");
10262
10801
  return;
10263
10802
  }
10264
10803
  const topicId = ctx.callbackQuery.message?.message_thread_id;
@@ -10344,7 +10883,7 @@ ${lines}`, { parse_mode: "HTML" }).catch(() => {
10344
10883
  }
10345
10884
  });
10346
10885
  }
10347
- var log32, STATIC_COMMANDS;
10886
+ var log33, STATIC_COMMANDS;
10348
10887
  var init_commands = __esm({
10349
10888
  "src/plugins/telegram/commands/index.ts"() {
10350
10889
  "use strict";
@@ -10370,7 +10909,7 @@ var init_commands = __esm({
10370
10909
  init_settings();
10371
10910
  init_doctor2();
10372
10911
  init_resume();
10373
- log32 = createChildLogger({ module: "telegram-menu-callbacks" });
10912
+ log33 = createChildLogger({ module: "telegram-menu-callbacks" });
10374
10913
  STATIC_COMMANDS = [
10375
10914
  { command: "new", description: "Create new session" },
10376
10915
  { command: "newchat", description: "New chat, same agent & workspace" },
@@ -10386,6 +10925,7 @@ var init_commands = __esm({
10386
10925
  { command: "restart", description: "Restart OpenACP" },
10387
10926
  { command: "update", description: "Update to latest version and restart" },
10388
10927
  { command: "doctor", description: "Run system diagnostics" },
10928
+ { command: "proxy", description: "Manage scoped proxy routing" },
10389
10929
  { command: "retry", description: "Re-check setup prerequisites (use if bot is stuck on startup)" },
10390
10930
  { command: "tunnel", description: "Create/stop tunnel for a local port" },
10391
10931
  { command: "tunnels", description: "List active tunnels" },
@@ -10406,14 +10946,14 @@ var init_commands = __esm({
10406
10946
  // src/plugins/telegram/permissions.ts
10407
10947
  import { InlineKeyboard as InlineKeyboard12 } from "grammy";
10408
10948
  import { nanoid as nanoid4 } from "nanoid";
10409
- var log33, PermissionHandler;
10949
+ var log34, PermissionHandler;
10410
10950
  var init_permissions = __esm({
10411
10951
  "src/plugins/telegram/permissions.ts"() {
10412
10952
  "use strict";
10413
10953
  init_formatting();
10414
10954
  init_topics();
10415
10955
  init_log();
10416
- log33 = createChildLogger({ module: "telegram-permissions" });
10956
+ log34 = createChildLogger({ module: "telegram-permissions" });
10417
10957
  PermissionHandler = class {
10418
10958
  constructor(bot, chatId, getSession, sendNotification) {
10419
10959
  this.bot = bot;
@@ -10491,7 +11031,7 @@ ${escapeHtml(request.description)}`,
10491
11031
  }
10492
11032
  const session = this.getSession(pending.sessionId);
10493
11033
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
10494
- log33.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
11034
+ log34.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
10495
11035
  if (session?.permissionGate.requestId === pending.requestId) {
10496
11036
  session.permissionGate.resolve(optionId);
10497
11037
  }
@@ -10511,7 +11051,7 @@ ${escapeHtml(request.description)}`,
10511
11051
  });
10512
11052
 
10513
11053
  // src/plugins/telegram/activity.ts
10514
- var log34, THINKING_REFRESH_MS, THINKING_MAX_MS, ThinkingIndicator, ToolCard, ActivityTracker2;
11054
+ var log35, THINKING_REFRESH_MS, THINKING_MAX_MS, ThinkingIndicator, ToolCard, ActivityTracker2;
10515
11055
  var init_activity = __esm({
10516
11056
  "src/plugins/telegram/activity.ts"() {
10517
11057
  "use strict";
@@ -10521,7 +11061,7 @@ var init_activity = __esm({
10521
11061
  init_stream_accumulator();
10522
11062
  init_stream_accumulator();
10523
11063
  init_display_spec_builder();
10524
- log34 = createChildLogger({ module: "telegram:activity" });
11064
+ log35 = createChildLogger({ module: "telegram:activity" });
10525
11065
  THINKING_REFRESH_MS = 15e3;
10526
11066
  THINKING_MAX_MS = 3 * 60 * 1e3;
10527
11067
  ThinkingIndicator = class {
@@ -10567,7 +11107,7 @@ var init_activity = __esm({
10567
11107
  }
10568
11108
  }
10569
11109
  } catch (err) {
10570
- log34.warn({ err }, "ThinkingIndicator.show() failed");
11110
+ log35.warn({ err }, "ThinkingIndicator.show() failed");
10571
11111
  } finally {
10572
11112
  this.sending = false;
10573
11113
  }
@@ -10744,7 +11284,7 @@ var init_activity = __esm({
10744
11284
  this.tracer?.log("telegram", { action: "telegram:delete:overflow", sessionId: this.sessionId, msgId: staleId });
10745
11285
  }
10746
11286
  } catch (err) {
10747
- log34.warn({ err }, "[ToolCard] send/edit failed");
11287
+ log35.warn({ err }, "[ToolCard] send/edit failed");
10748
11288
  }
10749
11289
  }
10750
11290
  };
@@ -10852,7 +11392,7 @@ var init_activity = __esm({
10852
11392
  const entry = this.toolStateMap.merge(id, status, rawInput, content, viewerLinks, diffStats);
10853
11393
  if (!existed || !entry) return;
10854
11394
  if (viewerLinks || entry.viewerLinks) {
10855
- log34.debug({ toolId: id, status, hasIncomingLinks: !!viewerLinks, hasEntryLinks: !!entry.viewerLinks, entryLinks: entry.viewerLinks }, "toolUpdate: viewer links trace");
11395
+ log35.debug({ toolId: id, status, hasIncomingLinks: !!viewerLinks, hasEntryLinks: !!entry.viewerLinks, entryLinks: entry.viewerLinks }, "toolUpdate: viewer links trace");
10856
11396
  }
10857
11397
  const spec = this.specBuilder.buildToolSpec(entry, this._outputMode, this.sessionContext);
10858
11398
  this.toolCard.updateFromSpec(spec);
@@ -11210,13 +11750,13 @@ var init_draft_manager = __esm({
11210
11750
  });
11211
11751
 
11212
11752
  // src/plugins/telegram/skill-command-manager.ts
11213
- var log35, SkillCommandManager;
11753
+ var log36, SkillCommandManager;
11214
11754
  var init_skill_command_manager = __esm({
11215
11755
  "src/plugins/telegram/skill-command-manager.ts"() {
11216
11756
  "use strict";
11217
11757
  init_commands();
11218
11758
  init_log();
11219
- log35 = createChildLogger({ module: "skill-commands" });
11759
+ log36 = createChildLogger({ module: "skill-commands" });
11220
11760
  SkillCommandManager = class {
11221
11761
  constructor(bot, chatId, sendQueue, sessionManager) {
11222
11762
  this.bot = bot;
@@ -11291,7 +11831,7 @@ var init_skill_command_manager = __esm({
11291
11831
  disable_notification: true
11292
11832
  });
11293
11833
  } catch (err) {
11294
- log35.error({ err, sessionId }, "Failed to send skill commands");
11834
+ log36.error({ err, sessionId }, "Failed to send skill commands");
11295
11835
  }
11296
11836
  }
11297
11837
  async cleanup(sessionId) {
@@ -11397,6 +11937,185 @@ var init_renderer2 = __esm({
11397
11937
  }
11398
11938
  });
11399
11939
 
11940
+ // src/plugins/identity/types.ts
11941
+ var init_types = __esm({
11942
+ "src/plugins/identity/types.ts"() {
11943
+ "use strict";
11944
+ }
11945
+ });
11946
+
11947
+ // src/core/commands/proxy.ts
11948
+ import { randomUUID as randomUUID3 } from "crypto";
11949
+ function clearProxyDraftsForChannel(channelId) {
11950
+ const prefix = `${channelId}:`;
11951
+ for (const [id, draft] of drafts) if (draft.owner.startsWith(prefix)) drafts.delete(id);
11952
+ }
11953
+ var DRAFT_TTL_MS, drafts;
11954
+ var init_proxy2 = __esm({
11955
+ "src/core/commands/proxy.ts"() {
11956
+ "use strict";
11957
+ init_proxy_types();
11958
+ init_types();
11959
+ init_proxy_store();
11960
+ init_proxy_service();
11961
+ DRAFT_TTL_MS = 10 * 6e4;
11962
+ drafts = /* @__PURE__ */ new Map();
11963
+ }
11964
+ });
11965
+
11966
+ // src/plugins/telegram/command-sync.ts
11967
+ import { createHash as createHash4 } from "crypto";
11968
+ function validDescription(description) {
11969
+ if (typeof description !== "string") return false;
11970
+ const trimmed = description.trim();
11971
+ return trimmed.length > 0 && trimmed.length <= TELEGRAM_DESCRIPTION_LIMIT;
11972
+ }
11973
+ function prepareTelegramCommandBoundary(coreCommands, registryCommands) {
11974
+ if (coreCommands.length > TELEGRAM_COMMAND_LIMIT) {
11975
+ throw new TelegramCommandBoundaryError("OpenACP core defines more than 100 Telegram commands");
11976
+ }
11977
+ const seen = /* @__PURE__ */ new Set();
11978
+ const commands = [];
11979
+ for (const command of coreCommands) {
11980
+ if (!TELEGRAM_COMMAND_NAME.test(command.command) || !validDescription(command.description)) {
11981
+ throw new TelegramCommandBoundaryError("OpenACP core contains an invalid Telegram command definition");
11982
+ }
11983
+ if (seen.has(command.command)) throw new TelegramCommandBoundaryError("OpenACP core contains a duplicate Telegram command definition");
11984
+ seen.add(command.command);
11985
+ commands.push({ command: command.command, description: command.description.trim() });
11986
+ }
11987
+ const skipped = { invalidName: 0, invalidDescription: 0, duplicate: 0, overflow: 0 };
11988
+ const validPlugins = [];
11989
+ for (const command of registryCommands) {
11990
+ if (command.category !== "plugin") continue;
11991
+ if (!TELEGRAM_COMMAND_NAME.test(command.name)) {
11992
+ skipped.invalidName++;
11993
+ continue;
11994
+ }
11995
+ if (!validDescription(command.description)) {
11996
+ skipped.invalidDescription++;
11997
+ continue;
11998
+ }
11999
+ if (seen.has(command.name) || validPlugins.some((entry) => entry.command === command.name)) {
12000
+ skipped.duplicate++;
12001
+ continue;
12002
+ }
12003
+ validPlugins.push({ command: command.name, description: command.description.trim() });
12004
+ }
12005
+ validPlugins.sort((a, b) => a.command.localeCompare(b.command) || a.description.localeCompare(b.description));
12006
+ const capacity = TELEGRAM_COMMAND_LIMIT - commands.length;
12007
+ skipped.overflow = Math.max(0, validPlugins.length - capacity);
12008
+ commands.push(...validPlugins.slice(0, capacity));
12009
+ return { commands, skipped };
12010
+ }
12011
+ function assertTelegramCommandBoundary(commands) {
12012
+ if (commands.length > TELEGRAM_COMMAND_LIMIT) throw new TelegramCommandBoundaryError("Telegram command list exceeds 100 entries");
12013
+ const seen = /* @__PURE__ */ new Set();
12014
+ for (const command of commands) {
12015
+ if (!TELEGRAM_COMMAND_NAME.test(command.command) || !validDescription(command.description) || seen.has(command.command)) {
12016
+ throw new TelegramCommandBoundaryError("Telegram command list failed validation before synchronization");
12017
+ }
12018
+ seen.add(command.command);
12019
+ }
12020
+ }
12021
+ function commandsEqual(current, desired) {
12022
+ return current.length === desired.length && current.every((command, index) => {
12023
+ const expected = desired[index];
12024
+ return command.command === expected?.command && command.description === expected.description;
12025
+ });
12026
+ }
12027
+ function throwIfAborted(signal) {
12028
+ if (signal?.aborted) throw signal.reason ?? new DOMException("Aborted", "AbortError");
12029
+ }
12030
+ function localeLabel(locale) {
12031
+ return locale || "neutral";
12032
+ }
12033
+ async function synchronizeTelegramCommands(api, chatId, desired, options) {
12034
+ if (!/^\d{1,20}$/.test(options.botId)) throw new Error("Telegram bot identity is invalid");
12035
+ assertTelegramCommandBoundary(desired);
12036
+ const chatKey = createHash4("sha256").update(String(chatId)).digest("hex").slice(0, 16);
12037
+ const scopes = [
12038
+ { name: "default", scope: { type: "default" }, ledgerName: "default" },
12039
+ { name: "chat", scope: { type: "chat", chat_id: chatId }, ledgerName: `chat:${chatKey}` },
12040
+ {
12041
+ name: "chat_administrators",
12042
+ scope: { type: "chat_administrators", chat_id: chatId },
12043
+ ledgerName: `chat_administrators:${chatKey}`,
12044
+ onlyWhenPresent: true
12045
+ }
12046
+ ];
12047
+ const desiredNames = new Set(desired.map((command) => command.command));
12048
+ const result = { updated: [], unchanged: [] };
12049
+ const initialConservative = await options.ownershipStore.withLock(async ({ ledger, conservative }) => {
12050
+ options.ownershipStore.claimOwner(ledger, options.botId, options.ownerIdentity, options.allowOwnerTakeover);
12051
+ options.ownershipStore.save(ledger);
12052
+ return conservative;
12053
+ });
12054
+ options.onOwnerClaimed?.();
12055
+ for (const locale of TELEGRAM_COMMAND_LOCALES) {
12056
+ for (const entry of scopes) {
12057
+ throwIfAborted(options.signal);
12058
+ const label = `${entry.name}:${localeLabel(locale)}`;
12059
+ const scopeKey = `${entry.ledgerName}|${localeLabel(locale)}`;
12060
+ const apiOptions = {
12061
+ scope: entry.scope,
12062
+ ...locale ? { language_code: locale } : {}
12063
+ };
12064
+ const current = await api.getMyCommands(apiOptions, options.signal);
12065
+ throwIfAborted(options.signal);
12066
+ const ownership = await options.ownershipStore.withLock(async ({ ledger, conservative }) => ({
12067
+ priorOwned: options.ownershipStore.getOwned(ledger, options.botId, scopeKey),
12068
+ conservative
12069
+ }));
12070
+ if (entry.onlyWhenPresent && current.length === 0) {
12071
+ await options.ownershipStore.withLock(async ({ ledger }) => {
12072
+ options.ownershipStore.claimOwner(ledger, options.botId, options.ownerIdentity);
12073
+ options.ownershipStore.setOwned(ledger, options.botId, scopeKey, []);
12074
+ options.ownershipStore.save(ledger);
12075
+ });
12076
+ result.unchanged.push(label);
12077
+ continue;
12078
+ }
12079
+ const owned = new Set(ownership.priorOwned ?? (initialConservative || ownership.conservative ? [] : options.historicalOwnedNames));
12080
+ const unmanaged = current.filter((command) => !desiredNames.has(command.command) && !owned.has(command.command));
12081
+ const merged = [...desired, ...unmanaged];
12082
+ if (merged.length > 100) {
12083
+ throw new Error(`Telegram command scope ${label} has no capacity for OpenACP commands without deleting unmanaged commands`);
12084
+ }
12085
+ if (!commandsEqual(current, merged)) {
12086
+ throwIfAborted(options.signal);
12087
+ await api.setMyCommands(merged, apiOptions, options.signal);
12088
+ result.updated.push(label);
12089
+ } else {
12090
+ result.unchanged.push(label);
12091
+ }
12092
+ throwIfAborted(options.signal);
12093
+ await options.ownershipStore.withLock(async ({ ledger }) => {
12094
+ options.ownershipStore.claimOwner(ledger, options.botId, options.ownerIdentity);
12095
+ options.ownershipStore.setOwned(ledger, options.botId, scopeKey, [...desiredNames]);
12096
+ options.ownershipStore.save(ledger);
12097
+ });
12098
+ }
12099
+ }
12100
+ return result;
12101
+ }
12102
+ var TelegramCommandBoundaryError, TELEGRAM_COMMAND_NAME, TELEGRAM_COMMAND_LIMIT, TELEGRAM_DESCRIPTION_LIMIT;
12103
+ var init_command_sync = __esm({
12104
+ "src/plugins/telegram/command-sync.ts"() {
12105
+ "use strict";
12106
+ init_telegram_command_scopes();
12107
+ TelegramCommandBoundaryError = class extends Error {
12108
+ constructor(message) {
12109
+ super(message);
12110
+ this.name = "TelegramCommandBoundaryError";
12111
+ }
12112
+ };
12113
+ TELEGRAM_COMMAND_NAME = /^[a-z0-9_]{1,32}$/;
12114
+ TELEGRAM_COMMAND_LIMIT = 100;
12115
+ TELEGRAM_DESCRIPTION_LIMIT = 256;
12116
+ }
12117
+ });
12118
+
11400
12119
  // src/plugins/telegram/validators.ts
11401
12120
  var validators_exports = {};
11402
12121
  __export(validators_exports, {
@@ -11471,7 +12190,7 @@ async function validateBotAdmin(token, chatId, telegramFetch) {
11471
12190
  };
11472
12191
  }
11473
12192
  const { status } = data.result;
11474
- log36.info(
12193
+ log37.info(
11475
12194
  { status, can_manage_topics: data.result.can_manage_topics, raw_result: data.result },
11476
12195
  "validateBotAdmin: getChatMember raw result"
11477
12196
  );
@@ -11491,7 +12210,7 @@ async function validateBotAdmin(token, chatId, telegramFetch) {
11491
12210
  }
11492
12211
  async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
11493
12212
  const issues = [];
11494
- log36.info({ chatId }, "checkTopicsPrerequisites: starting checks");
12213
+ log37.info({ chatId }, "checkTopicsPrerequisites: starting checks");
11495
12214
  try {
11496
12215
  const res = await telegramFetch(`https://api.telegram.org/bot${token}/getChat`, {
11497
12216
  method: "POST",
@@ -11499,7 +12218,7 @@ async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
11499
12218
  body: JSON.stringify({ chat_id: chatId })
11500
12219
  });
11501
12220
  const data = await res.json();
11502
- log36.info(
12221
+ log37.info(
11503
12222
  { chatId, apiOk: data.ok, is_forum: data.result?.is_forum, type: data.result?.type, title: data.result?.title },
11504
12223
  "checkTopicsPrerequisites: getChat result"
11505
12224
  );
@@ -11509,11 +12228,11 @@ async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
11509
12228
  );
11510
12229
  }
11511
12230
  } catch (err) {
11512
- log36.warn({ err, chatId }, "checkTopicsPrerequisites: getChat failed (network error)");
12231
+ log37.warn({ err, chatId }, "checkTopicsPrerequisites: getChat failed (network error)");
11513
12232
  issues.push("\u274C Could not check if Topics are enabled (network error).");
11514
12233
  }
11515
12234
  const adminResult = await validateBotAdmin(token, chatId, telegramFetch);
11516
- log36.info(
12235
+ log37.info(
11517
12236
  { chatId, adminOk: adminResult.ok, canManageTopics: adminResult.ok ? adminResult.canManageTopics : void 0, error: !adminResult.ok ? adminResult.error : void 0 },
11518
12237
  "checkTopicsPrerequisites: validateBotAdmin result"
11519
12238
  );
@@ -11528,17 +12247,17 @@ async function checkTopicsPrerequisites(token, chatId, telegramFetch) {
11528
12247
  '\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
12248
  );
11530
12249
  }
11531
- log36.info({ chatId, issueCount: issues.length, ok: issues.length === 0 }, "checkTopicsPrerequisites: result");
12250
+ log37.info({ chatId, issueCount: issues.length, ok: issues.length === 0 }, "checkTopicsPrerequisites: result");
11532
12251
  if (issues.length > 0) return { ok: false, issues };
11533
12252
  return { ok: true };
11534
12253
  }
11535
- var log36;
12254
+ var log37;
11536
12255
  var init_validators = __esm({
11537
12256
  "src/plugins/telegram/validators.ts"() {
11538
12257
  "use strict";
11539
12258
  init_log();
11540
12259
  init_network_redaction();
11541
- log36 = createChildLogger({ module: "telegram-validators" });
12260
+ log37 = createChildLogger({ module: "telegram-validators" });
11542
12261
  }
11543
12262
  });
11544
12263
 
@@ -11557,7 +12276,7 @@ function patchedFetch(input2, init, delegate) {
11557
12276
  }
11558
12277
  return delegate(input2, init);
11559
12278
  }
11560
- var log37, TelegramAdapter;
12279
+ var log38, HISTORICAL_OPENACP_COMMAND_NAMES, TelegramAdapter;
11561
12280
  var init_adapter = __esm({
11562
12281
  "src/plugins/telegram/adapter.ts"() {
11563
12282
  "use strict";
@@ -11577,7 +12296,19 @@ var init_adapter = __esm({
11577
12296
  init_messaging_adapter();
11578
12297
  init_renderer2();
11579
12298
  init_output_mode_resolver();
11580
- log37 = createChildLogger({ module: "telegram" });
12299
+ init_proxy2();
12300
+ init_network_redaction();
12301
+ init_command_sync();
12302
+ init_command_ownership_store();
12303
+ init_instance_context();
12304
+ log38 = createChildLogger({ module: "telegram" });
12305
+ HISTORICAL_OPENACP_COMMAND_NAMES = /* @__PURE__ */ new Set([
12306
+ ...STATIC_COMMANDS.map((command) => command.command),
12307
+ "clear",
12308
+ "bypass",
12309
+ "usage",
12310
+ "summary"
12311
+ ]);
11581
12312
  TelegramAdapter = class extends MessagingAdapter {
11582
12313
  name = "telegram";
11583
12314
  renderer = new TelegramRenderer();
@@ -11606,6 +12337,7 @@ var init_adapter = __esm({
11606
12337
  sessionTrackers = /* @__PURE__ */ new Map();
11607
12338
  callbackCache = /* @__PURE__ */ new Map();
11608
12339
  callbackCounter = 0;
12340
+ pendingCommandInputs = /* @__PURE__ */ new Map();
11609
12341
  /** Pending skill commands queued when session.threadId was not yet set */
11610
12342
  _pendingSkillCommands = /* @__PURE__ */ new Map();
11611
12343
  /** Control message IDs per session (for updating status text/buttons) */
@@ -11630,6 +12362,23 @@ var init_adapter = __esm({
11630
12362
  /** Set during normal shutdown so bot.stop() does not trigger a self-restart. */
11631
12363
  _stopping = false;
11632
12364
  unregisterProxyTester;
12365
+ /** Serializes command-list reconciliations triggered by startup and plugin hot reloads. */
12366
+ _commandSyncChain = Promise.resolve();
12367
+ _pendingCommandSnapshot;
12368
+ _commandSyncWorkerRunning = false;
12369
+ _commandSyncGeneration = 0;
12370
+ _commandSyncAbort;
12371
+ _commandSnapshotVersion = 0;
12372
+ _commandsReadyHandler;
12373
+ _latestCommandSnapshot;
12374
+ _commandApiReady = false;
12375
+ _initialCommandSyncScheduled = false;
12376
+ commandOwnershipStore = new TelegramCommandOwnershipStore(getGlobalRoot());
12377
+ commandOwnerIdentity;
12378
+ commandOwnerBotId;
12379
+ commandOwnerTakeoverRequested;
12380
+ _commandOwnerClaimed = false;
12381
+ _commandOwnerHeartbeat;
11633
12382
  telegramFetch() {
11634
12383
  return this.core.proxyService.createFetch("channels.telegram");
11635
12384
  }
@@ -11704,6 +12453,14 @@ var init_adapter = __esm({
11704
12453
  });
11705
12454
  this.core = core;
11706
12455
  this.telegramConfig = config;
12456
+ this.commandOwnerBotId = this.telegramConfig.botToken.split(":", 1)[0] ?? "";
12457
+ this.commandOwnerIdentity = {
12458
+ instanceId: this.core.instanceContext.id,
12459
+ instanceKey: telegramCommandInstanceKey(this.core.instanceContext.root),
12460
+ hostId: telegramCommandHostId(),
12461
+ pid: process.pid
12462
+ };
12463
+ this.commandOwnerTakeoverRequested = process.env.OPENACP_TELEGRAM_COMMAND_TAKEOVER === "1";
11707
12464
  this.saveTopicIds = saveTopicIds;
11708
12465
  this.unregisterProxyTester = this.core.proxyService.registerRouteTester(
11709
12466
  "channels.telegram",
@@ -11716,6 +12473,35 @@ var init_adapter = __esm({
11716
12473
  }
11717
12474
  );
11718
12475
  }
12476
+ /**
12477
+ * Capture command snapshots once startup begins. The normal boot path emits
12478
+ * SYSTEM_COMMANDS_READY before core.start(), so initial reconciliation reads the
12479
+ * authoritative registry state instead of relying on replay of that event. Once
12480
+ * the API is ready, later snapshots drive hot reloads.
12481
+ */
12482
+ subscribeToCommandSnapshots() {
12483
+ if (this._commandsReadyHandler) return;
12484
+ this._commandsReadyHandler = ({ commands }) => {
12485
+ this._latestCommandSnapshot = commands;
12486
+ if (this._commandApiReady) this.syncCommandsWithRetry(commands);
12487
+ };
12488
+ this.core.eventBus.on(BusEvent.SYSTEM_COMMANDS_READY, this._commandsReadyHandler);
12489
+ }
12490
+ /** Schedule one authoritative initial reconciliation after grammY is available. */
12491
+ scheduleInitialCommandSync() {
12492
+ if (this._initialCommandSyncScheduled) return;
12493
+ this._initialCommandSyncScheduled = true;
12494
+ const registry = this.core.lifecycleManager?.serviceRegistry?.get("command-registry");
12495
+ const commands = registry?.getAll() ?? this._latestCommandSnapshot;
12496
+ if (!commands) {
12497
+ log38.warn(
12498
+ { operation: "telegram-command-sync" },
12499
+ "Telegram command registry is not ready; waiting for a commands-ready snapshot"
12500
+ );
12501
+ return;
12502
+ }
12503
+ this.syncCommandsWithRetry(commands);
12504
+ }
11719
12505
  /**
11720
12506
  * Set up the grammY bot, register all callback and message handlers, then perform
11721
12507
  * two-phase startup: Phase 1 starts polling immediately; Phase 2 checks group
@@ -11724,6 +12510,11 @@ var init_adapter = __esm({
11724
12510
  */
11725
12511
  async start() {
11726
12512
  this._stopping = false;
12513
+ this._commandSyncGeneration++;
12514
+ this._commandSyncAbort = new AbortController();
12515
+ this._pendingCommandSnapshot = void 0;
12516
+ this._commandSyncWorkerRunning = false;
12517
+ this.subscribeToCommandSnapshots();
11727
12518
  this.bot = new Bot(this.telegramConfig.botToken, {
11728
12519
  client: {
11729
12520
  baseFetchConfig: { duplex: "half" },
@@ -11748,7 +12539,7 @@ var init_adapter = __esm({
11748
12539
  );
11749
12540
  this.bot.catch((err) => {
11750
12541
  const rootCause = err.error instanceof Error ? err.error : err;
11751
- log37.error({ err: rootCause }, "Telegram bot error");
12542
+ log38.error({ err: rootCause }, "Telegram bot error");
11752
12543
  });
11753
12544
  this.bot.api.config.use(async (prev, method, payload, signal) => {
11754
12545
  const maxRetries = 3;
@@ -11766,7 +12557,7 @@ var init_adapter = __esm({
11766
12557
  if (rateLimitedMethods.includes(method)) {
11767
12558
  this.sendQueue.onRateLimited();
11768
12559
  }
11769
- log37.warn(
12560
+ log38.warn(
11770
12561
  { method, retryAfter, attempt: attempt + 1 },
11771
12562
  "Rate limited by Telegram, retrying"
11772
12563
  );
@@ -11784,10 +12575,6 @@ var init_adapter = __esm({
11784
12575
  }
11785
12576
  return prev(method, payload, signal);
11786
12577
  });
11787
- const onCommandsReady = ({ commands }) => {
11788
- this.syncCommandsWithRetry(commands);
11789
- };
11790
- this.core.eventBus.on(BusEvent.SYSTEM_COMMANDS_READY, onCommandsReady);
11791
12578
  this.bot.use((ctx, next) => {
11792
12579
  const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
11793
12580
  if (chatId !== this.telegramConfig.chatId) return;
@@ -11801,7 +12588,52 @@ var init_adapter = __esm({
11801
12588
  );
11802
12589
  this.bot.on("message:text", async (ctx, next) => {
11803
12590
  const text3 = ctx.message?.text;
11804
- if (!text3?.startsWith("/")) return next();
12591
+ if (!text3) return next();
12592
+ const topicIdForInput = ctx.message.message_thread_id;
12593
+ const inputKey = `${ctx.chat.id}:${ctx.from?.id ?? 0}:${topicIdForInput ?? 0}`;
12594
+ const pending = this.pendingCommandInputs.get(inputKey);
12595
+ if (!text3.startsWith("/") && pending) {
12596
+ if (pending.promptMessageId !== void 0 && ctx.message.reply_to_message?.message_id !== pending.promptMessageId) {
12597
+ return next();
12598
+ }
12599
+ this.pendingCommandInputs.delete(inputKey);
12600
+ if (pending.expiresAt <= Date.now()) {
12601
+ await ctx.reply("This input request expired. Start the operation again.").catch(() => {
12602
+ });
12603
+ return;
12604
+ }
12605
+ if (pending.sensitive) {
12606
+ try {
12607
+ await ctx.deleteMessage();
12608
+ } catch {
12609
+ await ctx.reply(`\u26A0\uFE0F I could not securely remove that message, so its value was not used. ${pending.fallback}`).catch(() => {
12610
+ });
12611
+ return;
12612
+ }
12613
+ }
12614
+ const registry2 = this.core.lifecycleManager?.serviceRegistry?.get("command-registry");
12615
+ if (!registry2) return;
12616
+ const sessionId = topicIdForInput != null ? (await this.core.getOrResumeSession("telegram", String(topicIdForInput)))?.id ?? null : null;
12617
+ const response = await registry2.execute(pending.command, {
12618
+ raw: "",
12619
+ sessionId,
12620
+ channelId: "telegram",
12621
+ userId: String(ctx.from?.id),
12622
+ conversationId: `${ctx.chat.id}:${topicIdForInput ?? 0}`,
12623
+ interaction: {
12624
+ textInput: true,
12625
+ secureInput: "delete-after-capture",
12626
+ capturedInput: { value: text3, sensitive: pending.sensitive }
12627
+ },
12628
+ reply: async () => {
12629
+ }
12630
+ });
12631
+ if (response.type !== "silent" && response.type !== "delegated") {
12632
+ await this.renderCommandResponse(response, ctx.chat.id, topicIdForInput, String(ctx.from?.id));
12633
+ }
12634
+ return;
12635
+ }
12636
+ if (!text3.startsWith("/")) return next();
11805
12637
  const rawCmd = text3.split(" ")[0].slice(1).split("@")[0].toLowerCase();
11806
12638
  if (rawCmd === "retry") {
11807
12639
  await this.handleRetryCommand(ctx);
@@ -11847,6 +12679,8 @@ var init_adapter = __esm({
11847
12679
  sessionId,
11848
12680
  channelId: "telegram",
11849
12681
  userId: String(ctx.from?.id),
12682
+ conversationId: `${chatId}:${topicId ?? 0}`,
12683
+ interaction: { textInput: true, secureInput: "delete-after-capture" },
11850
12684
  reply: async (content) => {
11851
12685
  if (typeof content === "string") {
11852
12686
  await ctx.reply(content);
@@ -11854,7 +12688,8 @@ var init_adapter = __esm({
11854
12688
  await this.renderCommandResponse(
11855
12689
  content,
11856
12690
  chatId,
11857
- topicId
12691
+ topicId,
12692
+ String(ctx.from?.id)
11858
12693
  );
11859
12694
  }
11860
12695
  }
@@ -11865,7 +12700,7 @@ var init_adapter = __esm({
11865
12700
  if (response.type === "silent") {
11866
12701
  return next();
11867
12702
  }
11868
- await this.renderCommandResponse(response, chatId, topicId);
12703
+ await this.renderCommandResponse(response, chatId, topicId, String(ctx.from?.id));
11869
12704
  } catch (err) {
11870
12705
  await ctx.reply(`\u26A0\uFE0F Command failed: ${String(err)}`);
11871
12706
  }
@@ -11894,6 +12729,8 @@ var init_adapter = __esm({
11894
12729
  sessionId,
11895
12730
  channelId: "telegram",
11896
12731
  userId: String(ctx.from?.id),
12732
+ conversationId: `${chatId}:${topicId ?? 0}`,
12733
+ interaction: { textInput: true, secureInput: "delete-after-capture" },
11897
12734
  reply: async (content) => {
11898
12735
  if (typeof content === "string") {
11899
12736
  await ctx.editMessageText(content).catch(() => {
@@ -11902,7 +12739,11 @@ var init_adapter = __esm({
11902
12739
  }
11903
12740
  });
11904
12741
  await ctx.answerCallbackQuery();
11905
- if (response.type !== "silent") {
12742
+ if (response.type !== "silent" && response.type !== "delegated") {
12743
+ if (response.type === "input") {
12744
+ await this.renderCommandResponse(response, chatId, topicId, String(ctx.from?.id));
12745
+ return;
12746
+ }
11906
12747
  if (response.type === "menu") {
11907
12748
  const keyboard = response.options.map((opt) => [
11908
12749
  {
@@ -12010,32 +12851,34 @@ ${p}` : p;
12010
12851
  }
12011
12852
  );
12012
12853
  this.setupRoutes();
12854
+ this._commandApiReady = true;
12855
+ this.scheduleInitialCommandSync();
12013
12856
  void this.bot.start({
12014
12857
  allowed_updates: ["message", "callback_query"],
12015
- onStart: () => log37.info({ chatId: this.telegramConfig.chatId }, "Telegram bot started")
12858
+ onStart: () => log38.info({ chatId: this.telegramConfig.chatId }, "Telegram bot started")
12016
12859
  }).catch((err) => {
12017
12860
  if (this._stopping) return;
12018
12861
  const inner = err?.error;
12019
12862
  const rootCause = err instanceof Error ? err : inner instanceof Error ? inner : err;
12020
- log37.error({ err: rootCause }, "Telegram polling stopped unexpectedly");
12863
+ log38.error({ err: rootCause }, "Telegram polling stopped unexpectedly");
12021
12864
  if (this.core.requestRestart) {
12022
12865
  const requestRestart = this.core.requestRestart;
12023
- log37.error("Restarting OpenACP because Telegram polling stopped");
12866
+ log38.error("Restarting OpenACP because Telegram polling stopped");
12024
12867
  setImmediate(() => {
12025
12868
  if (this._stopping) return;
12026
12869
  void requestRestart().catch((restartErr) => {
12027
- log37.error({ err: restartErr }, "OpenACP restart request failed after Telegram polling stopped");
12870
+ log38.error({ err: restartErr }, "OpenACP restart request failed after Telegram polling stopped");
12028
12871
  });
12029
12872
  });
12030
12873
  } else {
12031
- log37.error("Exiting because Telegram polling stopped and no restart hook is available");
12874
+ log38.error("Exiting because Telegram polling stopped and no restart hook is available");
12032
12875
  setImmediate(() => {
12033
12876
  if (this._stopping) return;
12034
12877
  process.exit(1);
12035
12878
  });
12036
12879
  }
12037
12880
  });
12038
- log37.info(
12881
+ log38.info(
12039
12882
  {
12040
12883
  chatId: this.telegramConfig.chatId,
12041
12884
  notificationTopicId: this.telegramConfig.notificationTopicId,
@@ -12050,12 +12893,12 @@ ${p}` : p;
12050
12893
  this.telegramFetch()
12051
12894
  );
12052
12895
  if (prereqResult.ok) {
12053
- log37.info("Telegram adapter: prerequisites OK, initializing topic-dependent features");
12896
+ log38.info("Telegram adapter: prerequisites OK, initializing topic-dependent features");
12054
12897
  await this.initTopicDependentFeatures();
12055
12898
  } else {
12056
- log37.warn({ issues: prereqResult.issues }, "Telegram adapter: prerequisites NOT met, starting watcher");
12899
+ log38.warn({ issues: prereqResult.issues }, "Telegram adapter: prerequisites NOT met, starting watcher");
12057
12900
  for (const issue of prereqResult.issues) {
12058
- log37.warn({ issue }, "Telegram prerequisite not met");
12901
+ log38.warn({ issue }, "Telegram prerequisite not met");
12059
12902
  }
12060
12903
  this.startPrerequisiteWatcher(prereqResult.issues);
12061
12904
  }
@@ -12071,7 +12914,7 @@ ${p}` : p;
12071
12914
  } catch (err) {
12072
12915
  if (attempt === maxRetries) throw err;
12073
12916
  const delay = baseDelayMs * Math.pow(2, attempt - 1);
12074
- log37.warn(
12917
+ log38.warn(
12075
12918
  { err, attempt, maxRetries, delayMs: delay, operation: label },
12076
12919
  `${label} failed, retrying in ${delay}ms`
12077
12920
  );
@@ -12086,21 +12929,126 @@ ${p}` : p;
12086
12929
  * from the registry, deduplicating by command name. Non-critical.
12087
12930
  */
12088
12931
  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)");
12932
+ this._pendingCommandSnapshot = registryCommands;
12933
+ this._commandSnapshotVersion++;
12934
+ if (this._commandSyncWorkerRunning) return;
12935
+ this._commandSyncWorkerRunning = true;
12936
+ const generation = this._commandSyncGeneration;
12937
+ const signal = this._commandSyncAbort?.signal;
12938
+ this._commandSyncChain = (async () => {
12939
+ while (this._pendingCommandSnapshot && generation === this._commandSyncGeneration && !signal?.aborted) {
12940
+ const snapshot = this._pendingCommandSnapshot;
12941
+ const snapshotVersion = this._commandSnapshotVersion;
12942
+ this._pendingCommandSnapshot = void 0;
12943
+ let allCommands;
12944
+ try {
12945
+ const boundary = prepareTelegramCommandBoundary(STATIC_COMMANDS, snapshot);
12946
+ allCommands = boundary.commands;
12947
+ const { invalidName, invalidDescription, duplicate, overflow } = boundary.skipped;
12948
+ if (invalidName || invalidDescription || duplicate || overflow) {
12949
+ log38.warn(
12950
+ { invalidName, invalidDescription, duplicate, overflow, operation: "telegram-command-boundary" },
12951
+ "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."
12952
+ );
12953
+ }
12954
+ if (!allCommands.some((command) => command.command === "proxy")) {
12955
+ throw new TelegramCommandBoundaryError("Required OpenACP command /proxy is missing");
12956
+ }
12957
+ } catch (error) {
12958
+ log38.error(
12959
+ { error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)), operation: "telegram-command-boundary" },
12960
+ "OpenACP core Telegram command definitions are invalid; command sync was skipped without changing Telegram state"
12961
+ );
12962
+ continue;
12963
+ }
12964
+ const maxAttempts = 5;
12965
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
12966
+ if (generation !== this._commandSyncGeneration || signal?.aborted) return;
12967
+ try {
12968
+ const result = await synchronizeTelegramCommands(
12969
+ this.bot.api,
12970
+ this.telegramConfig.chatId,
12971
+ allCommands,
12972
+ {
12973
+ ownershipStore: this.commandOwnershipStore,
12974
+ botId: this.commandOwnerBotId,
12975
+ ownerIdentity: this.commandOwnerIdentity,
12976
+ allowOwnerTakeover: this.commandOwnerTakeoverRequested,
12977
+ onOwnerClaimed: () => {
12978
+ this._commandOwnerClaimed = true;
12979
+ this.startCommandOwnerHeartbeat();
12980
+ },
12981
+ historicalOwnedNames: HISTORICAL_OPENACP_COMMAND_NAMES,
12982
+ signal
12983
+ }
12984
+ );
12985
+ if (generation !== this._commandSyncGeneration || signal?.aborted) return;
12986
+ log38.info(
12987
+ { updatedScopes: result.updated, unchangedScopes: result.unchanged },
12988
+ "Telegram command menus synchronized"
12989
+ );
12990
+ break;
12991
+ } catch (err) {
12992
+ if (generation !== this._commandSyncGeneration || signal?.aborted) return;
12993
+ if (err instanceof TelegramCommandOwnerConflictError) {
12994
+ log38.warn(
12995
+ { code: err.code, ownerInstanceId: err.ownerInstanceId, instanceId: this.commandOwnerIdentity.instanceId, operation: "telegram-command-sync" },
12996
+ "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."
12997
+ );
12998
+ break;
12999
+ }
13000
+ if (snapshotVersion !== this._commandSnapshotVersion && this._pendingCommandSnapshot) break;
13001
+ const safeError2 = redactNetworkSecrets(err instanceof Error ? err.message : String(err));
13002
+ if (attempt === maxAttempts) {
13003
+ log38.warn(
13004
+ { error: safeError2, attempt, maxAttempts, operation: "telegram-command-sync" },
13005
+ "Telegram command menu sync failed; bot remains online. Run /doctor and inspect Telegram connectivity"
13006
+ );
13007
+ break;
13008
+ }
13009
+ const delayMs = 2e3 * Math.pow(2, attempt - 1);
13010
+ log38.warn(
13011
+ { error: safeError2, attempt, maxAttempts, delayMs, operation: "telegram-command-sync" },
13012
+ "Telegram command menu sync failed, retrying"
13013
+ );
13014
+ await new Promise((resolve7) => {
13015
+ const timer = setTimeout(resolve7, delayMs);
13016
+ signal?.addEventListener("abort", () => {
13017
+ clearTimeout(timer);
13018
+ resolve7();
13019
+ }, { once: true });
13020
+ });
13021
+ }
13022
+ }
13023
+ }
13024
+ })().finally(() => {
13025
+ this._commandSyncWorkerRunning = false;
13026
+ if (this._pendingCommandSnapshot && generation === this._commandSyncGeneration && !signal?.aborted) {
13027
+ this.syncCommandsWithRetry(this._pendingCommandSnapshot);
13028
+ }
12099
13029
  });
12100
13030
  }
13031
+ startCommandOwnerHeartbeat() {
13032
+ if (this._commandOwnerHeartbeat) return;
13033
+ this._commandOwnerHeartbeat = setInterval(() => {
13034
+ void this.commandOwnershipStore.heartbeatOwner(this.commandOwnerBotId, this.commandOwnerIdentity).then((owned) => {
13035
+ if (!owned) {
13036
+ this._commandOwnerClaimed = false;
13037
+ log38.warn(
13038
+ { instanceId: this.commandOwnerIdentity.instanceId, operation: "telegram-command-owner-heartbeat" },
13039
+ "Telegram command-sync ownership was lost; no further command-list writes will be attempted until ownership is resolved."
13040
+ );
13041
+ }
13042
+ }).catch((error) => log38.warn(
13043
+ { error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)), operation: "telegram-command-owner-heartbeat" },
13044
+ "Could not refresh Telegram command-sync ownership heartbeat"
13045
+ ));
13046
+ }, 3e4);
13047
+ this._commandOwnerHeartbeat.unref?.();
13048
+ }
12101
13049
  async initTopicDependentFeatures() {
12102
13050
  if (this._topicsInitialized) return;
12103
- log37.info(
13051
+ log38.info(
12104
13052
  { notificationTopicId: this.telegramConfig.notificationTopicId, assistantTopicId: this.telegramConfig.assistantTopicId },
12105
13053
  "initTopicDependentFeatures: starting (existing IDs in config)"
12106
13054
  );
@@ -12125,7 +13073,7 @@ ${p}` : p;
12125
13073
  this.assistantTopicId = topics.assistantTopicId;
12126
13074
  this._systemTopicIds.notificationTopicId = topics.notificationTopicId;
12127
13075
  this._systemTopicIds.assistantTopicId = topics.assistantTopicId;
12128
- log37.info(
13076
+ log38.info(
12129
13077
  { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId },
12130
13078
  "initTopicDependentFeatures: topics ready"
12131
13079
  );
@@ -12156,7 +13104,7 @@ ${p}` : p;
12156
13104
  ).then((msg) => {
12157
13105
  if (msg) this.storeControlMsgId(sessionId, msg.message_id);
12158
13106
  }).catch((err) => {
12159
- log37.warn({ err, sessionId }, "Failed to send initial messages for new session");
13107
+ log38.warn({ err, sessionId }, "Failed to send initial messages for new session");
12160
13108
  });
12161
13109
  };
12162
13110
  this.core.eventBus.on(BusEvent.SESSION_THREAD_READY, this._threadReadyHandler);
@@ -12198,7 +13146,7 @@ ${p}` : p;
12198
13146
  this._queueNotifications.delete(data.turnId);
12199
13147
  if (result) {
12200
13148
  this.bot.api.deleteMessage(this.telegramConfig.chatId, result.message_id).catch((err) => {
12201
- log37.warn({ err }, "Failed to delete queue notification after race");
13149
+ log38.warn({ err }, "Failed to delete queue notification after race");
12202
13150
  });
12203
13151
  }
12204
13152
  } else if (result) {
@@ -12208,7 +13156,7 @@ ${p}` : p;
12208
13156
  }
12209
13157
  } catch (err) {
12210
13158
  this._queueNotifications.delete(data.turnId);
12211
- log37.warn({ err }, "Failed to send queue notification");
13159
+ log38.warn({ err }, "Failed to send queue notification");
12212
13160
  }
12213
13161
  });
12214
13162
  this.core.eventBus.on(BusEvent.MESSAGE_PROCESSING, async (data) => {
@@ -12217,13 +13165,13 @@ ${p}` : p;
12217
13165
  if (typeof entry === "number") {
12218
13166
  this._queueNotifications.delete(data.turnId);
12219
13167
  this.bot.api.deleteMessage(this.telegramConfig.chatId, entry).catch((err) => {
12220
- log37.warn({ err }, "Failed to delete queue notification on processing");
13168
+ log38.warn({ err }, "Failed to delete queue notification on processing");
12221
13169
  });
12222
13170
  } else if (entry === "pending") {
12223
13171
  this._queueNotifications.set(data.turnId, "delete");
12224
13172
  }
12225
13173
  });
12226
- log37.info({ assistantTopicId: this.assistantTopicId }, "initTopicDependentFeatures: sending welcome message");
13174
+ log38.info({ assistantTopicId: this.assistantTopicId }, "initTopicDependentFeatures: sending welcome message");
12227
13175
  try {
12228
13176
  const config = this.core.configManager.get();
12229
13177
  const agents = this.core.agentManager.getAvailableAgents();
@@ -12245,17 +13193,17 @@ ${p}` : p;
12245
13193
  this.core.lifecycleManager?.serviceRegistry?.get("menu-registry")
12246
13194
  )
12247
13195
  });
12248
- log37.info("initTopicDependentFeatures: welcome message sent");
13196
+ log38.info("initTopicDependentFeatures: welcome message sent");
12249
13197
  } catch (err) {
12250
- log37.warn({ err }, "Failed to send welcome message");
13198
+ log38.warn({ err }, "Failed to send welcome message");
12251
13199
  }
12252
13200
  try {
12253
13201
  await this.core.assistantManager.getOrSpawn("telegram", String(this.assistantTopicId));
12254
13202
  } catch (err) {
12255
- log37.error({ err }, "Failed to spawn assistant");
13203
+ log38.error({ err }, "Failed to spawn assistant");
12256
13204
  }
12257
13205
  this._topicsInitialized = true;
12258
- log37.info("Telegram adapter fully initialized");
13206
+ log38.info("Telegram adapter fully initialized");
12259
13207
  }
12260
13208
  /**
12261
13209
  * Handle the /retry command — re-runs prerequisite checks immediately.
@@ -12292,7 +13240,7 @@ ${p}` : p;
12292
13240
  ).catch(() => {
12293
13241
  });
12294
13242
  } catch (err) {
12295
- log37.error({ err }, "handleRetryCommand: init failed after prerequisites met");
13243
+ log38.error({ err }, "handleRetryCommand: init failed after prerequisites met");
12296
13244
  await ctx.reply("\u26A0\uFE0F Prerequisites are met but initialization failed. Check server logs and try /retry again.").catch(() => {
12297
13245
  });
12298
13246
  }
@@ -12318,7 +13266,7 @@ OpenACP will automatically retry every 30 seconds. After fixing the issues above
12318
13266
  this.bot.api.sendMessage(this.telegramConfig.chatId, setupMessage, {
12319
13267
  parse_mode: "HTML"
12320
13268
  }).catch((err) => {
12321
- log37.warn({ err }, "Failed to send setup guidance to General topic");
13269
+ log38.warn({ err }, "Failed to send setup guidance to General topic");
12322
13270
  });
12323
13271
  const schedule = [5e3, 1e4, 3e4];
12324
13272
  let attempt = 1;
@@ -12331,7 +13279,7 @@ OpenACP will automatically retry every 30 seconds. After fixing the issues above
12331
13279
  this.telegramFetch()
12332
13280
  );
12333
13281
  if (result.ok) {
12334
- log37.info("Prerequisites met \u2014 completing Telegram adapter initialization");
13282
+ log38.info("Prerequisites met \u2014 completing Telegram adapter initialization");
12335
13283
  try {
12336
13284
  await this.initTopicDependentFeatures();
12337
13285
  this._prerequisiteWatcher = null;
@@ -12344,14 +13292,14 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12344
13292
  { parse_mode: "HTML" }
12345
13293
  );
12346
13294
  } catch (err) {
12347
- log37.error({ err }, "Failed to complete initialization after prerequisites met \u2014 will retry");
13295
+ log38.error({ err }, "Failed to complete initialization after prerequisites met \u2014 will retry");
12348
13296
  const delay2 = schedule[Math.min(attempt, schedule.length - 1)];
12349
13297
  attempt++;
12350
13298
  this._prerequisiteWatcher = setTimeout(retry, delay2);
12351
13299
  }
12352
13300
  return;
12353
13301
  }
12354
- log37.debug({ issues: result.issues }, "Prerequisites not yet met, retrying");
13302
+ log38.debug({ issues: result.issues }, "Prerequisites not yet met, retrying");
12355
13303
  const delay = schedule[Math.min(attempt, schedule.length - 1)];
12356
13304
  attempt++;
12357
13305
  this._prerequisiteWatcher = setTimeout(retry, delay);
@@ -12367,6 +13315,14 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12367
13315
  */
12368
13316
  async stop() {
12369
13317
  this._stopping = true;
13318
+ this._commandSyncGeneration++;
13319
+ this._commandSyncAbort?.abort();
13320
+ this._commandSyncAbort = void 0;
13321
+ this._pendingCommandSnapshot = void 0;
13322
+ if (this._commandOwnerHeartbeat) {
13323
+ clearInterval(this._commandOwnerHeartbeat);
13324
+ this._commandOwnerHeartbeat = void 0;
13325
+ }
12370
13326
  this.unregisterProxyTester?.();
12371
13327
  this.unregisterProxyTester = void 0;
12372
13328
  if (this._prerequisiteWatcher !== null) {
@@ -12385,12 +13341,36 @@ System topics have been created. Tap${assistantTopicLink} to get started.`,
12385
13341
  this.core.eventBus.off("session:configChanged", this._configChangedHandler);
12386
13342
  this._configChangedHandler = void 0;
12387
13343
  }
13344
+ if (this._commandsReadyHandler) {
13345
+ this.core.eventBus.off(BusEvent.SYSTEM_COMMANDS_READY, this._commandsReadyHandler);
13346
+ this._commandsReadyHandler = void 0;
13347
+ }
13348
+ this._commandApiReady = false;
13349
+ this._initialCommandSyncScheduled = false;
13350
+ this._latestCommandSnapshot = void 0;
12388
13351
  this.sendQueue.clear();
12389
- await this.bot.stop();
12390
- log37.info("Telegram bot stopped");
13352
+ this.pendingCommandInputs.clear();
13353
+ this.callbackCache.clear();
13354
+ clearProxyDraftsForChannel("telegram");
13355
+ try {
13356
+ await this.bot.stop();
13357
+ } finally {
13358
+ if (this._commandOwnerClaimed) {
13359
+ try {
13360
+ await this.commandOwnershipStore.releaseOwner(this.commandOwnerBotId, this.commandOwnerIdentity);
13361
+ } catch (error) {
13362
+ log38.warn(
13363
+ { error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)), operation: "telegram-command-owner-release" },
13364
+ "Could not mark Telegram command-sync ownership as stopped; a same-host explicit takeover may be required on restart"
13365
+ );
13366
+ }
13367
+ this._commandOwnerClaimed = false;
13368
+ }
13369
+ }
13370
+ log38.info("Telegram bot stopped");
12391
13371
  }
12392
13372
  // --- CommandRegistry response rendering ---
12393
- async renderCommandResponse(response, chatId, topicId) {
13373
+ async renderCommandResponse(response, chatId, topicId, userId) {
12394
13374
  switch (response.type) {
12395
13375
  case "text":
12396
13376
  await this.bot.api.sendMessage(chatId, response.text, {
@@ -12458,6 +13438,34 @@ ${lines.join("\n")}`;
12458
13438
  });
12459
13439
  break;
12460
13440
  }
13441
+ case "input": {
13442
+ if (!userId) {
13443
+ await this.bot.api.sendMessage(chatId, response.fallback, { message_thread_id: topicId });
13444
+ break;
13445
+ }
13446
+ const key = `${chatId}:${userId}:${topicId ?? 0}`;
13447
+ for (const [pendingKey, pending] of this.pendingCommandInputs) {
13448
+ if (pending.expiresAt <= Date.now()) this.pendingCommandInputs.delete(pendingKey);
13449
+ }
13450
+ while (this.pendingCommandInputs.size >= 1e3) {
13451
+ const oldest = this.pendingCommandInputs.keys().next().value;
13452
+ if (!oldest) break;
13453
+ this.pendingCommandInputs.delete(oldest);
13454
+ }
13455
+ 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." : "";
13456
+ const promptMessage = await this.bot.api.sendMessage(chatId, `${response.prompt}${warning}`, {
13457
+ message_thread_id: topicId,
13458
+ reply_markup: { force_reply: true, selective: true }
13459
+ });
13460
+ this.pendingCommandInputs.set(key, {
13461
+ command: response.command,
13462
+ sensitive: response.sensitive === true,
13463
+ fallback: response.fallback,
13464
+ expiresAt: Date.now() + (response.expiresInMs ?? 10 * 6e4),
13465
+ promptMessageId: promptMessage.message_id
13466
+ });
13467
+ break;
13468
+ }
12461
13469
  case "silent":
12462
13470
  break;
12463
13471
  }
@@ -12532,7 +13540,7 @@ ${lines.join("\n")}`;
12532
13540
  username: ctx.from.username
12533
13541
  }
12534
13542
  }
12535
- ).catch((err) => log37.error({ err }, "handleMessage error"));
13543
+ ).catch((err) => log38.error({ err }, "handleMessage error"));
12536
13544
  });
12537
13545
  this.bot.on("message:photo", async (ctx) => {
12538
13546
  const threadId = ctx.message.message_thread_id;
@@ -12638,7 +13646,7 @@ ${lines.join("\n")}`;
12638
13646
  if (session.archiving) return;
12639
13647
  const threadId = Number(session.threadId);
12640
13648
  if (!threadId || isNaN(threadId)) {
12641
- log37.warn(
13649
+ log38.warn(
12642
13650
  { sessionId, threadId: session.threadId },
12643
13651
  "Session has no valid threadId, skipping message"
12644
13652
  );
@@ -12654,7 +13662,7 @@ ${lines.join("\n")}`;
12654
13662
  this._sessionThreadIds.delete(sessionId);
12655
13663
  }
12656
13664
  }).catch((err) => {
12657
- log37.warn({ err, sessionId }, "Dispatch queue error");
13665
+ log38.warn({ err, sessionId }, "Dispatch queue error");
12658
13666
  });
12659
13667
  this._dispatchQueues.set(sessionId, next);
12660
13668
  await next;
@@ -12784,7 +13792,7 @@ ${lines.join("\n")}`;
12784
13792
  usageMsgId = result?.message_id;
12785
13793
  if (usageMsgId) this.usageMsgIds.set(sessionId, usageMsgId);
12786
13794
  } catch (err) {
12787
- log37.warn({ err, sessionId }, "Failed to send usage message");
13795
+ log38.warn({ err, sessionId }, "Failed to send usage message");
12788
13796
  }
12789
13797
  }
12790
13798
  async handleAttachment(sessionId, content) {
@@ -12793,7 +13801,7 @@ ${lines.join("\n")}`;
12793
13801
  if (!content.attachment) return;
12794
13802
  const { attachment } = content;
12795
13803
  if (attachment.size > 50 * 1024 * 1024) {
12796
- log37.warn(
13804
+ log38.warn(
12797
13805
  {
12798
13806
  sessionId,
12799
13807
  fileName: attachment.fileName,
@@ -12837,7 +13845,7 @@ ${lines.join("\n")}`;
12837
13845
  );
12838
13846
  }
12839
13847
  } catch (err) {
12840
- log37.error(
13848
+ log38.error(
12841
13849
  { err, sessionId, fileName: attachment.fileName },
12842
13850
  "Failed to send attachment"
12843
13851
  );
@@ -12962,7 +13970,7 @@ Task completed.
12962
13970
  */
12963
13971
  async sendPermissionRequest(sessionId, request) {
12964
13972
  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");
13973
+ log38.info({ sessionId, requestId: request.id }, "Permission request sent");
12966
13974
  const session = this.core.sessionManager.getSession(sessionId);
12967
13975
  if (!session) return;
12968
13976
  await this.sendQueue.enqueue(
@@ -12977,7 +13985,7 @@ Task completed.
12977
13985
  async sendNotification(notification) {
12978
13986
  this.getTracer(notification.sessionId)?.log("telegram", { action: "notification:send", sessionId: notification.sessionId, type: notification.type });
12979
13987
  if (notification.sessionId === this.core.assistantManager?.get("telegram")?.id) return;
12980
- log37.info(
13988
+ log38.info(
12981
13989
  { sessionId: notification.sessionId, type: notification.type },
12982
13990
  "Notification sent"
12983
13991
  );
@@ -13023,14 +14031,14 @@ Task completed.
13023
14031
  */
13024
14032
  async createSessionThread(sessionId, name) {
13025
14033
  this.getTracer(sessionId)?.log("telegram", { action: "thread:create", sessionId, name });
13026
- log37.info({ sessionId, name }, "Session topic created");
14034
+ log38.info({ sessionId, name }, "Session topic created");
13027
14035
  const threadId = await createSessionTopic(this.bot, this.telegramConfig.chatId, name);
13028
14036
  this.bot.api.sendMessage(
13029
14037
  this.telegramConfig.chatId,
13030
14038
  "\u23F3 Setting up session, please wait...",
13031
14039
  { message_thread_id: threadId, parse_mode: "HTML" }
13032
14040
  ).catch((err) => {
13033
- log37.warn({ err, sessionId, threadId }, "Failed to send setup message to new topic");
14041
+ log38.warn({ err, sessionId, threadId }, "Failed to send setup message to new topic");
13034
14042
  });
13035
14043
  return String(threadId);
13036
14044
  }
@@ -13044,7 +14052,7 @@ Task completed.
13044
14052
  if (!session) return;
13045
14053
  const threadId = Number(session.threadId);
13046
14054
  if (!threadId) {
13047
- log37.debug({ sessionId, newName }, "Cannot rename thread \u2014 threadId not set yet");
14055
+ log38.debug({ sessionId, newName }, "Cannot rename thread \u2014 threadId not set yet");
13048
14056
  return;
13049
14057
  }
13050
14058
  await renameSessionTopic(
@@ -13064,7 +14072,7 @@ Task completed.
13064
14072
  try {
13065
14073
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
13066
14074
  } catch (err) {
13067
- log37.warn(
14075
+ log38.warn(
13068
14076
  { err, sessionId, topicId },
13069
14077
  "Failed to delete forum topic (may already be deleted)"
13070
14078
  );
@@ -13113,7 +14121,7 @@ Task completed.
13113
14121
  const buffer = Buffer.from(await response.arrayBuffer());
13114
14122
  return { buffer, filePath: file.file_path };
13115
14123
  } catch (err) {
13116
- log37.error({ err }, "Failed to download file from Telegram");
14124
+ log38.error({ err }, "Failed to download file from Telegram");
13117
14125
  return null;
13118
14126
  }
13119
14127
  }
@@ -13134,7 +14142,7 @@ Task completed.
13134
14142
  try {
13135
14143
  buffer = await this.fileService.convertOggToWav(buffer);
13136
14144
  } catch (err) {
13137
- log37.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
14145
+ log38.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
13138
14146
  fileName = "voice.ogg";
13139
14147
  mimeType = "audio/ogg";
13140
14148
  originalFilePath = void 0;
@@ -13169,7 +14177,7 @@ Task completed.
13169
14177
  userId: String(userId),
13170
14178
  text: text3,
13171
14179
  attachments: [att]
13172
- }).catch((err) => log37.error({ err }, "handleMessage error"));
14180
+ }).catch((err) => log38.error({ err }, "handleMessage error"));
13173
14181
  }
13174
14182
  /**
13175
14183
  * Remove skill slash commands from the Telegram bot command list for a session.
@@ -15275,7 +16283,7 @@ var TTS_BLOCK_REGEX = /\[TTS\]([\s\S]*?)\[\/TTS\]/;
15275
16283
  var TTS_MAX_LENGTH = 5e3;
15276
16284
  var TTS_TIMEOUT_MS = 3e4;
15277
16285
  var VALID_TRANSITIONS = {
15278
- initializing: /* @__PURE__ */ new Set(["active", "error"]),
16286
+ initializing: /* @__PURE__ */ new Set(["active", "error", "cancelled"]),
15279
16287
  active: /* @__PURE__ */ new Set(["error", "finished", "cancelled"]),
15280
16288
  error: /* @__PURE__ */ new Set(["active", "cancelled"]),
15281
16289
  cancelled: /* @__PURE__ */ new Set(["active"]),
@@ -15337,6 +16345,9 @@ var Session = class extends TypedEmitter {
15337
16345
  _abortedTurnIds = /* @__PURE__ */ new Set();
15338
16346
  /** Last completed turnId — used by abortPrompt() to retroactively mark a just-finished turn as interrupted */
15339
16347
  _lastCompletedTurnId = null;
16348
+ /** Idempotent teardown checkpoints allow cleanup retry after a partial failure. */
16349
+ _destroyedAgent = false;
16350
+ _closedLogger = false;
15340
16351
  _autoApprovedCommands = [];
15341
16352
  get autoApprovedCommands() {
15342
16353
  return this._autoApprovedCommands;
@@ -15415,7 +16426,7 @@ var Session = class extends TypedEmitter {
15415
16426
  this.transition("finished");
15416
16427
  this.emit(SessionEv.SESSION_END, reason ?? "completed");
15417
16428
  }
15418
- /** Transition to cancelled — from active or error (terminal session cancel) */
16429
+ /** Transition to cancelled — from initializing, active, or error. */
15419
16430
  markCancelled() {
15420
16431
  this.transition("cancelled");
15421
16432
  }
@@ -15932,8 +16943,14 @@ ${result.text}` : result.text;
15932
16943
  this.permissionGate.reject("Session destroyed");
15933
16944
  }
15934
16945
  this.queue.clear();
15935
- await this.agentInstance.destroy();
15936
- await closeSessionLogger(this.log);
16946
+ if (!this._destroyedAgent) {
16947
+ await this.agentInstance.destroy();
16948
+ this._destroyedAgent = true;
16949
+ }
16950
+ if (!this._closedLogger) {
16951
+ await closeSessionLogger(this.log);
16952
+ this._closedLogger = true;
16953
+ }
15937
16954
  }
15938
16955
  };
15939
16956
 
@@ -16451,6 +17468,9 @@ var MessageTransformer = class {
16451
17468
 
16452
17469
  // src/core/sessions/session-manager.ts
16453
17470
  init_events();
17471
+ init_log();
17472
+ init_network_redaction();
17473
+ var log7 = createChildLogger({ module: "session-manager" });
16454
17474
  var SessionManager = class {
16455
17475
  sessions = /* @__PURE__ */ new Map();
16456
17476
  store;
@@ -16460,6 +17480,7 @@ var SessionManager = class {
16460
17480
  // Prevents SessionBridge STATUS_CHANGE listeners from overwriting the flushed state
16461
17481
  // with transient error status from in-flight prompts that fail after shutdown.
16462
17482
  _shutdownComplete = false;
17483
+ cancellationOps = /* @__PURE__ */ new Map();
16463
17484
  /**
16464
17485
  * Inject the EventBus after construction. Deferred because EventBus is created
16465
17486
  * after SessionManager during bootstrap, so it cannot be passed to the constructor.
@@ -16560,30 +17581,76 @@ var SessionManager = class {
16560
17581
  getSessionRecord(sessionId) {
16561
17582
  return this.store?.get(sessionId);
16562
17583
  }
16563
- /** Cancel a session: abort in-flight prompt, transition to cancelled, destroy agent, and persist. */
17584
+ /**
17585
+ * Cancel a session exactly once. Concurrent callers share one operation;
17586
+ * terminal sessions return an idempotent result instead of throwing.
17587
+ */
16564
17588
  async cancelSession(sessionId) {
17589
+ const existing = this.cancellationOps.get(sessionId);
17590
+ if (existing) return existing;
17591
+ const operation = this.performCancelSession(sessionId);
17592
+ this.cancellationOps.set(sessionId, operation);
17593
+ try {
17594
+ return await operation;
17595
+ } finally {
17596
+ if (this.cancellationOps.get(sessionId) === operation) this.cancellationOps.delete(sessionId);
17597
+ }
17598
+ }
17599
+ async performCancelSession(sessionId) {
16565
17600
  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);
17601
+ const recordBefore = this.store?.get(sessionId);
17602
+ const previousStatus = session?.status ?? recordBefore?.status;
17603
+ if (!previousStatus) {
17604
+ const error = new Error(`Session "${sessionId}" not found`);
17605
+ error.code = "SESSION_NOT_FOUND";
17606
+ throw error;
16576
17607
  }
16577
- if (this.store) {
17608
+ const alreadyTerminal = previousStatus === "cancelled" || previousStatus === "finished";
17609
+ const finalStatus = previousStatus === "finished" ? "finished" : "cancelled";
17610
+ if (this.store && finalStatus === "cancelled") {
16578
17611
  const record = this.store.get(sessionId);
16579
- if (record && record.status !== "cancelled" && record.status !== "finished") {
17612
+ if (record && record.status !== "cancelled") {
16580
17613
  await this.store.save({ ...record, status: "cancelled" });
17614
+ this.store.flush();
16581
17615
  }
16582
17616
  }
16583
- if (this.middlewareChain) {
17617
+ let cleanupPending = false;
17618
+ if (session) {
17619
+ if (!alreadyTerminal) {
17620
+ session.markCancelled();
17621
+ try {
17622
+ await session.abortPrompt();
17623
+ } catch (error) {
17624
+ log7.warn(
17625
+ { sessionId, error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)) },
17626
+ "Session prompt abort failed after cancellation became durable; continuing cleanup"
17627
+ );
17628
+ }
17629
+ }
17630
+ try {
17631
+ await session.destroy();
17632
+ this.sessions.delete(sessionId);
17633
+ } catch (error) {
17634
+ cleanupPending = true;
17635
+ log7.warn(
17636
+ { sessionId, error: redactNetworkSecrets(error instanceof Error ? error.message : String(error)) },
17637
+ "Session is durably cancelled but agent/logger cleanup failed; repeat cancellation to retry cleanup"
17638
+ );
17639
+ }
17640
+ }
17641
+ if (!cleanupPending && this.middlewareChain) {
16584
17642
  this.middlewareChain.execute(Hook.SESSION_AFTER_DESTROY, { sessionId }, async (p) => p).catch(() => {
16585
17643
  });
16586
17644
  }
17645
+ return {
17646
+ sessionId,
17647
+ cancelled: !alreadyTerminal,
17648
+ previousStatus,
17649
+ status: finalStatus,
17650
+ alreadyTerminal,
17651
+ cleanupPending,
17652
+ ...cleanupPending ? { warning: "Session state is cancelled and persisted; process cleanup is pending. Repeat cancellation to retry cleanup." } : {}
17653
+ };
16587
17654
  }
16588
17655
  /** List live (in-memory) sessions, optionally filtered by channel. Excludes assistant sessions. */
16589
17656
  listSessions(channelId) {
@@ -16752,7 +17819,7 @@ init_bypass_detection();
16752
17819
  import micromatch from "micromatch";
16753
17820
  init_events();
16754
17821
  var { isMatch } = micromatch;
16755
- var log7 = createChildLogger({ module: "session-bridge" });
17822
+ var log8 = createChildLogger({ module: "session-bridge" });
16756
17823
  var SessionBridge = class {
16757
17824
  constructor(session, adapter, deps, adapterId) {
16758
17825
  this.session = session;
@@ -16784,16 +17851,16 @@ var SessionBridge = class {
16784
17851
  if (!result) return;
16785
17852
  this.tracer?.log("core", { step: "dispatch", sessionId, message: result.message });
16786
17853
  this.adapter.sendMessage(sessionId, result.message).catch((err) => {
16787
- log7.error({ err, sessionId }, "Failed to send message to adapter");
17854
+ log8.error({ err, sessionId }, "Failed to send message to adapter");
16788
17855
  });
16789
17856
  } else {
16790
17857
  this.tracer?.log("core", { step: "dispatch", sessionId, message });
16791
17858
  this.adapter.sendMessage(sessionId, message).catch((err) => {
16792
- log7.error({ err, sessionId }, "Failed to send message to adapter");
17859
+ log8.error({ err, sessionId }, "Failed to send message to adapter");
16793
17860
  });
16794
17861
  }
16795
17862
  } catch (err) {
16796
- log7.error({ err, sessionId }, "Error in sendMessage middleware");
17863
+ log8.error({ err, sessionId }, "Error in sendMessage middleware");
16797
17864
  }
16798
17865
  }
16799
17866
  /**
@@ -16839,7 +17906,7 @@ var SessionBridge = class {
16839
17906
  try {
16840
17907
  await this.adapter.sendPermissionRequest(this.session.id, request);
16841
17908
  } catch (err) {
16842
- log7.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
17909
+ log8.error({ err, sessionId: this.session.id, adapterId: this.adapterId }, "Failed to send permission request to adapter");
16843
17910
  }
16844
17911
  });
16845
17912
  this.listen(this.session, SessionEv.STATUS_CHANGE, (from, to) => {
@@ -16924,14 +17991,14 @@ var SessionBridge = class {
16924
17991
  try {
16925
17992
  this.handleAgentEvent(event);
16926
17993
  } catch (err) {
16927
- log7.error({ err, sessionId: this.session.id }, "Error handling agent event (middleware fallback)");
17994
+ log8.error({ err, sessionId: this.session.id }, "Error handling agent event (middleware fallback)");
16928
17995
  }
16929
17996
  }
16930
17997
  } else {
16931
17998
  try {
16932
17999
  this.handleAgentEvent(event);
16933
18000
  } catch (err) {
16934
- log7.error({ err, sessionId: this.session.id }, "Error handling agent event");
18001
+ log8.error({ err, sessionId: this.session.id }, "Error handling agent event");
16935
18002
  }
16936
18003
  }
16937
18004
  }
@@ -16984,40 +18051,40 @@ var SessionBridge = class {
16984
18051
  break;
16985
18052
  case "image_content": {
16986
18053
  if (this.deps.fileService) {
16987
- const fs37 = this.deps.fileService;
18054
+ const fs38 = this.deps.fileService;
16988
18055
  const sid = this.session.id;
16989
18056
  const { data, mimeType } = event;
16990
18057
  const buffer = Buffer.from(data, "base64");
16991
- const ext = fs37.extensionFromMime(mimeType);
16992
- fs37.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
18058
+ const ext = fs38.extensionFromMime(mimeType);
18059
+ fs38.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
16993
18060
  this.sendMessage(sid, {
16994
18061
  type: "attachment",
16995
18062
  text: "",
16996
18063
  attachment: att
16997
18064
  });
16998
- }).catch((err) => log7.error({ err }, "Failed to save agent image"));
18065
+ }).catch((err) => log8.error({ err }, "Failed to save agent image"));
16999
18066
  }
17000
18067
  break;
17001
18068
  }
17002
18069
  case "audio_content": {
17003
18070
  if (this.deps.fileService) {
17004
- const fs37 = this.deps.fileService;
18071
+ const fs38 = this.deps.fileService;
17005
18072
  const sid = this.session.id;
17006
18073
  const { data, mimeType } = event;
17007
18074
  const buffer = Buffer.from(data, "base64");
17008
- const ext = fs37.extensionFromMime(mimeType);
17009
- fs37.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
18075
+ const ext = fs38.extensionFromMime(mimeType);
18076
+ fs38.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
17010
18077
  this.sendMessage(sid, {
17011
18078
  type: "attachment",
17012
18079
  text: "",
17013
18080
  attachment: att
17014
18081
  });
17015
- }).catch((err) => log7.error({ err }, "Failed to save agent audio"));
18082
+ }).catch((err) => log8.error({ err }, "Failed to save agent audio"));
17016
18083
  }
17017
18084
  break;
17018
18085
  }
17019
18086
  case "commands_update":
17020
- log7.debug({ commands: event.commands }, "Commands available");
18087
+ log8.debug({ commands: event.commands }, "Commands available");
17021
18088
  this.adapter.sendSkillCommands?.(this.session.id, event.commands);
17022
18089
  break;
17023
18090
  case "system_message":
@@ -17114,7 +18181,7 @@ var SessionBridge = class {
17114
18181
  if (isAgentBypass || isClientBypass) {
17115
18182
  const allowOption = request.options.find((o) => o.isAllow);
17116
18183
  if (allowOption) {
17117
- log7.info(
18184
+ log8.info(
17118
18185
  { sessionId: this.session.id, requestId: request.id, optionId: allowOption.id, agentBypass: !!isAgentBypass, clientBypass: !!isClientBypass },
17119
18186
  "Bypass mode: auto-approving permission"
17120
18187
  );
@@ -17127,7 +18194,7 @@ var SessionBridge = class {
17127
18194
  if (isMatch(normalized, patterns, { dot: true })) {
17128
18195
  const allowOption = request.options.find((o) => o.isAllow);
17129
18196
  if (allowOption) {
17130
- log7.info(
18197
+ log8.info(
17131
18198
  { sessionId: this.session.id, requestId: request.id, command: request.description },
17132
18199
  "autoApprovedCommands: auto-approving matching command"
17133
18200
  );
@@ -17155,7 +18222,7 @@ var SessionBridge = class {
17155
18222
  // src/core/sessions/session-factory.ts
17156
18223
  init_log();
17157
18224
  init_events();
17158
- var log8 = createChildLogger({ module: "session-factory" });
18225
+ var log9 = createChildLogger({ module: "session-factory" });
17159
18226
  function toSetConfigOptionValue(option) {
17160
18227
  return option.type === "boolean" ? { type: "boolean", value: option.currentValue } : { type: "select", value: option.currentValue };
17161
18228
  }
@@ -17229,7 +18296,7 @@ var SessionFactory = class {
17229
18296
  configAllowedPaths
17230
18297
  );
17231
18298
  } catch (resumeErr) {
17232
- log8.warn(
18299
+ log9.warn(
17233
18300
  { agentName: createParams.agentName, resumeErr },
17234
18301
  "Agent session resume failed, falling back to fresh spawn"
17235
18302
  );
@@ -17382,10 +18449,10 @@ var SessionFactory = class {
17382
18449
  }
17383
18450
  }
17384
18451
  }
17385
- log8.info({ sessionId }, "Lazy resume by ID successful");
18452
+ log9.info({ sessionId }, "Lazy resume by ID successful");
17386
18453
  return session;
17387
18454
  } catch (err) {
17388
- log8.error({ err, sessionId }, "Lazy resume by ID failed");
18455
+ log9.error({ err, sessionId }, "Lazy resume by ID failed");
17389
18456
  return null;
17390
18457
  } finally {
17391
18458
  this.resumeLocks.delete(sessionId);
@@ -17410,19 +18477,19 @@ var SessionFactory = class {
17410
18477
  (p) => String(p.topicId) === threadId || String(p.threadId ?? "") === threadId
17411
18478
  );
17412
18479
  if (!record) {
17413
- log8.debug({ threadId, channelId }, "No session record found for thread");
18480
+ log9.debug({ threadId, channelId }, "No session record found for thread");
17414
18481
  return null;
17415
18482
  }
17416
18483
  if (record.isAssistant) return null;
17417
18484
  if (record.status === "error" || record.status === "cancelled") {
17418
- log8.warn(
18485
+ log9.warn(
17419
18486
  { threadId, sessionId: record.sessionId, status: record.status },
17420
18487
  "Session record found but skipped (status: %s) \u2014 use /new to start a fresh session",
17421
18488
  record.status
17422
18489
  );
17423
18490
  return null;
17424
18491
  }
17425
- log8.info({ threadId, sessionId: record.sessionId, status: record.status }, "Lazy resume: found record, attempting resume");
18492
+ log9.info({ threadId, sessionId: record.sessionId, status: record.status }, "Lazy resume: found record, attempting resume");
17426
18493
  const resumePromise = (async () => {
17427
18494
  try {
17428
18495
  const session = await this.createFullSession({
@@ -17474,7 +18541,7 @@ var SessionFactory = class {
17474
18541
  }
17475
18542
  const resumeFalledBack = record.agentSessionId && session.agentSessionId !== record.agentSessionId;
17476
18543
  if (resumeFalledBack) {
17477
- log8.info({ sessionId: session.id }, "Resume fell back to fresh spawn \u2014 injecting conversation history");
18544
+ log9.info({ sessionId: session.id }, "Resume fell back to fresh spawn \u2014 injecting conversation history");
17478
18545
  const contextManager = this.getContextManager?.();
17479
18546
  if (contextManager) {
17480
18547
  try {
@@ -17491,10 +18558,10 @@ var SessionFactory = class {
17491
18558
  }
17492
18559
  }
17493
18560
  }
17494
- log8.info({ sessionId: session.id, threadId }, "Lazy resume successful");
18561
+ log9.info({ sessionId: session.id, threadId }, "Lazy resume successful");
17495
18562
  return session;
17496
18563
  } catch (err) {
17497
- log8.error({ err, record }, "Lazy resume failed");
18564
+ log9.error({ err, record }, "Lazy resume failed");
17498
18565
  const adapter = this.adapters?.get(channelId);
17499
18566
  if (adapter) {
17500
18567
  try {
@@ -17520,7 +18587,7 @@ var SessionFactory = class {
17520
18587
  }
17521
18588
  const config = this.configManager.get();
17522
18589
  const resolvedAgent = agentName || config.defaultAgent;
17523
- log8.info({ channelId, agentName: resolvedAgent }, "New session request");
18590
+ log9.info({ channelId, agentName: resolvedAgent }, "New session request");
17524
18591
  const agentDef = this.agentCatalog.resolve(resolvedAgent);
17525
18592
  const resolvedWorkspace = this.configManager.resolveWorkspace(
17526
18593
  workspacePath || agentDef?.workingDirectory
@@ -17570,7 +18637,7 @@ var SessionFactory = class {
17570
18637
  params.contextOptions
17571
18638
  );
17572
18639
  } catch (err) {
17573
- log8.warn({ err }, "Context building failed, proceeding without context");
18640
+ log9.warn({ err }, "Context building failed, proceeding without context");
17574
18641
  }
17575
18642
  }
17576
18643
  const session = await this.createFullSession({
@@ -17625,7 +18692,7 @@ import { nanoid as nanoid3 } from "nanoid";
17625
18692
  init_log();
17626
18693
  import fs9 from "fs";
17627
18694
  import path8 from "path";
17628
- var log9 = createChildLogger({ module: "session-store" });
18695
+ var log10 = createChildLogger({ module: "session-store" });
17629
18696
  var DEBOUNCE_MS = 2e3;
17630
18697
  var JsonFileSessionStore = class {
17631
18698
  records = /* @__PURE__ */ new Map();
@@ -17739,7 +18806,7 @@ var JsonFileSessionStore = class {
17739
18806
  fs9.readFileSync(this.filePath, "utf-8")
17740
18807
  );
17741
18808
  if (raw.version !== 1) {
17742
- log9.warn(
18809
+ log10.warn(
17743
18810
  { version: raw.version },
17744
18811
  "Unknown session store version, skipping load"
17745
18812
  );
@@ -17748,9 +18815,9 @@ var JsonFileSessionStore = class {
17748
18815
  for (const [id, record] of Object.entries(raw.sessions)) {
17749
18816
  this.records.set(id, this.migrateRecord(record));
17750
18817
  }
17751
- log9.debug({ count: this.records.size }, "Loaded session records");
18818
+ log10.debug({ count: this.records.size }, "Loaded session records");
17752
18819
  } catch (err) {
17753
- log9.error({ err }, "Failed to load session store, backing up corrupt file");
18820
+ log10.error({ err }, "Failed to load session store, backing up corrupt file");
17754
18821
  try {
17755
18822
  fs9.renameSync(this.filePath, `${this.filePath}.bak`);
17756
18823
  } catch {
@@ -17793,7 +18860,7 @@ var JsonFileSessionStore = class {
17793
18860
  }
17794
18861
  }
17795
18862
  if (removed > 0) {
17796
- log9.info({ removed }, "Cleaned up expired session records");
18863
+ log10.info({ removed }, "Cleaned up expired session records");
17797
18864
  this.scheduleDiskWrite();
17798
18865
  }
17799
18866
  }
@@ -17812,7 +18879,7 @@ init_agent_registry();
17812
18879
  init_agent_registry();
17813
18880
  init_log();
17814
18881
  init_events();
17815
- var log10 = createChildLogger({ module: "agent-switch" });
18882
+ var log11 = createChildLogger({ module: "agent-switch" });
17816
18883
  var AgentSwitchHandler = class {
17817
18884
  constructor(deps) {
17818
18885
  this.deps = deps;
@@ -17893,7 +18960,7 @@ var AgentSwitchHandler = class {
17893
18960
  resumed = true;
17894
18961
  return instance2;
17895
18962
  } catch {
17896
- log10.warn({ sessionId, toAgent }, "Resume failed, falling back to new agent with context injection");
18963
+ log11.warn({ sessionId, toAgent }, "Resume failed, falling back to new agent with context injection");
17897
18964
  }
17898
18965
  }
17899
18966
  const instance = await agentManager.spawn(toAgent, session.workingDirectory, configAllowedPaths);
@@ -17962,10 +19029,10 @@ var AgentSwitchHandler = class {
17962
19029
  createBridge(session, adapter, adapterId).connect();
17963
19030
  }
17964
19031
  }
17965
- log10.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
19032
+ log11.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
17966
19033
  } catch (rollbackErr) {
17967
19034
  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");
19035
+ log11.error({ sessionId, fromAgent, toAgent, err, rollbackErr }, "Agent switch failed and rollback also failed");
17969
19036
  }
17970
19037
  throw err;
17971
19038
  }
@@ -17975,7 +19042,7 @@ var AgentSwitchHandler = class {
17975
19042
  if (adapter) {
17976
19043
  createBridge(session, adapter, adapterId).connect();
17977
19044
  } else {
17978
- log10.warn({ sessionId, adapterId }, "Adapter not available during switch reconnect, skipping bridge");
19045
+ log11.warn({ sessionId, adapterId }, "Adapter not available during switch reconnect, skipping bridge");
17979
19046
  }
17980
19047
  }
17981
19048
  }
@@ -18003,7 +19070,7 @@ init_agent_dependencies();
18003
19070
  init_log();
18004
19071
  import * as fs12 from "fs";
18005
19072
  import * as path11 from "path";
18006
- var log12 = createChildLogger({ module: "agent-catalog" });
19073
+ var log13 = createChildLogger({ module: "agent-catalog" });
18007
19074
  var REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
18008
19075
  var DEFAULT_TTL_HOURS = 24;
18009
19076
  var AgentCatalog = class {
@@ -18037,7 +19104,7 @@ var AgentCatalog = class {
18037
19104
  /** Fetch the latest agent registry from the CDN and update the local cache. */
18038
19105
  async fetchRegistry() {
18039
19106
  try {
18040
- log12.info("Fetching agent registry from CDN...");
19107
+ log13.info("Fetching agent registry from CDN...");
18041
19108
  const response = await this.scopedFetch(this.registryUrl);
18042
19109
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
18043
19110
  const data = await response.json();
@@ -18050,9 +19117,9 @@ var AgentCatalog = class {
18050
19117
  fs12.mkdirSync(path11.dirname(this.cachePath), { recursive: true });
18051
19118
  fs12.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2), { mode: 384 });
18052
19119
  this.enrichInstalledFromRegistry();
18053
- log12.info({ count: this.registryAgents.length }, "Registry updated");
19120
+ log13.info({ count: this.registryAgents.length }, "Registry updated");
18054
19121
  } catch (err) {
18055
- log12.warn({ err }, "Failed to fetch registry, using cached data");
19122
+ log13.warn({ err }, "Failed to fetch registry, using cached data");
18056
19123
  }
18057
19124
  }
18058
19125
  /** Re-fetch registry only if the local cache has expired (24-hour TTL). */
@@ -18260,7 +19327,7 @@ var AgentCatalog = class {
18260
19327
  }
18261
19328
  }
18262
19329
  if (changed) {
18263
- log12.info("Enriched installed agents with registry data");
19330
+ log13.info("Enriched installed agents with registry data");
18264
19331
  }
18265
19332
  }
18266
19333
  isCacheStale() {
@@ -18280,11 +19347,11 @@ var AgentCatalog = class {
18280
19347
  const raw = JSON.parse(fs12.readFileSync(this.cachePath, "utf-8"));
18281
19348
  if (raw.data?.agents) {
18282
19349
  this.registryAgents = raw.data.agents;
18283
- log12.debug({ count: this.registryAgents.length }, "Loaded registry from cache");
19350
+ log13.debug({ count: this.registryAgents.length }, "Loaded registry from cache");
18284
19351
  return;
18285
19352
  }
18286
19353
  } catch {
18287
- log12.warn("Failed to load registry cache");
19354
+ log13.warn("Failed to load registry cache");
18288
19355
  }
18289
19356
  }
18290
19357
  try {
@@ -18297,13 +19364,13 @@ var AgentCatalog = class {
18297
19364
  if (fs12.existsSync(candidate)) {
18298
19365
  const raw = JSON.parse(fs12.readFileSync(candidate, "utf-8"));
18299
19366
  this.registryAgents = raw.agents ?? [];
18300
- log12.debug({ count: this.registryAgents.length }, "Loaded registry from bundled snapshot");
19367
+ log13.debug({ count: this.registryAgents.length }, "Loaded registry from bundled snapshot");
18301
19368
  return;
18302
19369
  }
18303
19370
  }
18304
- log12.warn("No registry data available (no cache, no snapshot)");
19371
+ log13.warn("No registry data available (no cache, no snapshot)");
18305
19372
  } catch {
18306
- log12.warn("Failed to load bundled registry snapshot");
19373
+ log13.warn("Failed to load bundled registry snapshot");
18307
19374
  }
18308
19375
  }
18309
19376
  };
@@ -18332,7 +19399,7 @@ init_log();
18332
19399
  import * as fs13 from "fs";
18333
19400
  import * as path12 from "path";
18334
19401
  import { z as z2 } from "zod";
18335
- var log13 = createChildLogger({ module: "agent-store" });
19402
+ var log14 = createChildLogger({ module: "agent-store" });
18336
19403
  var InstalledAgentSchema = z2.object({
18337
19404
  registryId: z2.string().nullable(),
18338
19405
  name: z2.string(),
@@ -18368,11 +19435,11 @@ var AgentStore = class {
18368
19435
  if (result.success) {
18369
19436
  this.data = result.data;
18370
19437
  } else {
18371
- log13.warn({ errors: result.error.issues }, "Invalid agents.json, starting fresh");
19438
+ log14.warn({ errors: result.error.issues }, "Invalid agents.json, starting fresh");
18372
19439
  this.data = { version: 1, installed: {} };
18373
19440
  }
18374
19441
  } catch (err) {
18375
- log13.warn({ err }, "Failed to read agents.json, starting fresh");
19442
+ log14.warn({ err }, "Failed to read agents.json, starting fresh");
18376
19443
  this.data = { version: 1, installed: {} };
18377
19444
  }
18378
19445
  }
@@ -18827,7 +19894,7 @@ function createPluginContext(opts) {
18827
19894
  }
18828
19895
  };
18829
19896
  const baseLog = opts.log ?? noopLog;
18830
- const log38 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
19897
+ const log39 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
18831
19898
  const storageImpl = new PluginStorageImpl(storagePath);
18832
19899
  const storage = {
18833
19900
  async get(key) {
@@ -18897,7 +19964,7 @@ function createPluginContext(opts) {
18897
19964
  const ctx = {
18898
19965
  pluginName,
18899
19966
  pluginConfig,
18900
- log: log38,
19967
+ log: log39,
18901
19968
  storage,
18902
19969
  on(event, handler) {
18903
19970
  requirePermission(permissions, "events:read", "on()");
@@ -18932,7 +19999,7 @@ function createPluginContext(opts) {
18932
19999
  const registry = serviceRegistry.get("command-registry");
18933
20000
  if (registry && typeof registry.register === "function") {
18934
20001
  registry.register(def, pluginName);
18935
- log38.debug(`Command '/${def.name}' registered`);
20002
+ log39.debug(`Command '/${def.name}' registered`);
18936
20003
  }
18937
20004
  },
18938
20005
  async sendMessage(_sessionId, _content) {
@@ -18995,7 +20062,7 @@ function createPluginContext(opts) {
18995
20062
  const registry = serviceRegistry.get("field-registry");
18996
20063
  if (registry && typeof registry.register === "function") {
18997
20064
  registry.register(pluginName, fields);
18998
- log38.debug(`Registered ${fields.length} editable field(s) for ${pluginName}`);
20065
+ log39.debug(`Registered ${fields.length} editable field(s) for ${pluginName}`);
18999
20066
  }
19000
20067
  },
19001
20068
  get sessions() {
@@ -19335,7 +20402,7 @@ var LifecycleManager = class {
19335
20402
 
19336
20403
  // src/core/menu-registry.ts
19337
20404
  init_log();
19338
- var log14 = createChildLogger({ module: "menu-registry" });
20405
+ var log15 = createChildLogger({ module: "menu-registry" });
19339
20406
  var MenuRegistry = class {
19340
20407
  items = /* @__PURE__ */ new Map();
19341
20408
  /** Register or replace a menu item by its unique ID. */
@@ -19357,7 +20424,7 @@ var MenuRegistry = class {
19357
20424
  try {
19358
20425
  return item.visible();
19359
20426
  } catch (err) {
19360
- log14.warn({ err, id: item.id }, "MenuItem visible() threw, hiding item");
20427
+ log15.warn({ err, id: item.id }, "MenuItem visible() threw, hiding item");
19361
20428
  return false;
19362
20429
  }
19363
20430
  }).sort((a, b) => a.priority - b.priority);
@@ -19425,7 +20492,7 @@ openacp api new claude ~/project
19425
20492
  }
19426
20493
 
19427
20494
  // src/core/assistant/assistant-registry.ts
19428
- var log15 = createChildLogger({ module: "assistant-registry" });
20495
+ var log16 = createChildLogger({ module: "assistant-registry" });
19429
20496
  var AssistantRegistry = class {
19430
20497
  sections = /* @__PURE__ */ new Map();
19431
20498
  _instanceRoot = "";
@@ -19436,7 +20503,7 @@ var AssistantRegistry = class {
19436
20503
  /** Register a prompt section. Overwrites any existing section with the same id. */
19437
20504
  register(section) {
19438
20505
  if (this.sections.has(section.id)) {
19439
- log15.warn({ id: section.id }, "Assistant section overwritten");
20506
+ log16.warn({ id: section.id }, "Assistant section overwritten");
19440
20507
  }
19441
20508
  this.sections.set(section.id, section);
19442
20509
  }
@@ -19472,7 +20539,7 @@ ${context}`);
19472
20539
  parts.push("```bash\n" + cmds + "\n```");
19473
20540
  }
19474
20541
  } catch (err) {
19475
- log15.warn({ err, sectionId: section.id }, "Assistant section buildContext() failed, skipping");
20542
+ log16.warn({ err, sectionId: section.id }, "Assistant section buildContext() failed, skipping");
19476
20543
  }
19477
20544
  }
19478
20545
  parts.push(buildAssistantGuidelines(this._instanceRoot));
@@ -19482,7 +20549,7 @@ ${context}`);
19482
20549
 
19483
20550
  // src/core/assistant/assistant-manager.ts
19484
20551
  init_log();
19485
- var log16 = createChildLogger({ module: "assistant-manager" });
20552
+ var log17 = createChildLogger({ module: "assistant-manager" });
19486
20553
  var AssistantManager = class {
19487
20554
  constructor(core, registry) {
19488
20555
  this.core = core;
@@ -19513,7 +20580,7 @@ var AssistantManager = class {
19513
20580
  this.sessions.set(channelId, session);
19514
20581
  const systemPrompt = this.registry.buildSystemPrompt(channelId);
19515
20582
  this.pendingSystemPrompts.set(channelId, systemPrompt);
19516
- log16.info(
20583
+ log17.info(
19517
20584
  { sessionId: session.id, channelId, reused: !!existing },
19518
20585
  existing ? "Assistant session reused (system prompt deferred)" : "Assistant spawned (system prompt deferred)"
19519
20586
  );
@@ -19704,6 +20771,13 @@ function registerCoreMenuItems(registry) {
19704
20771
  group: "config",
19705
20772
  action: { type: "command", command: "/integrate" }
19706
20773
  });
20774
+ registry.register({
20775
+ id: "core:proxy",
20776
+ label: "\u{1F310} Proxy Routing",
20777
+ priority: 32,
20778
+ group: "config",
20779
+ action: { type: "command", command: "/proxy" }
20780
+ });
19707
20781
  registry.register({
19708
20782
  id: "core:restart",
19709
20783
  label: "\u{1F504} Restart",
@@ -19739,7 +20813,7 @@ init_log();
19739
20813
  init_native_stt();
19740
20814
  init_events();
19741
20815
  init_proxy_service();
19742
- var log17 = createChildLogger({ module: "core" });
20816
+ var log18 = createChildLogger({ module: "core" });
19743
20817
  var OpenACPCore = class {
19744
20818
  configManager;
19745
20819
  agentCatalog;
@@ -19822,7 +20896,7 @@ var OpenACPCore = class {
19822
20896
  this.proxyService.onRouteChanged(async (scope) => {
19823
20897
  if (scope === "global" || scope === "agents.default" || scope.startsWith("agents.")) {
19824
20898
  await this.agentManager.destroyWarm();
19825
- log17.info({ scope }, "Proxy route changed; idle agent warm pool invalidated");
20899
+ log18.info({ scope }, "Proxy route changed; idle agent warm pool invalidated");
19826
20900
  }
19827
20901
  });
19828
20902
  const storePath = ctx.paths.sessions;
@@ -19884,7 +20958,7 @@ var OpenACPCore = class {
19884
20958
  if (configPath === "logging.level" && typeof value === "string") {
19885
20959
  const { setLogLevel: setLogLevel2 } = await Promise.resolve().then(() => (init_log(), log_exports));
19886
20960
  setLogLevel2(value);
19887
- log17.info({ level: value }, "Log level changed at runtime");
20961
+ log18.info({ level: value }, "Log level changed at runtime");
19888
20962
  }
19889
20963
  if (configPath.startsWith("speech.")) {
19890
20964
  const speechSvc = this.lifecycleManager.serviceRegistry.get("speech");
@@ -19894,7 +20968,7 @@ var OpenACPCore = class {
19894
20968
  const pluginCfg = await settingsMgr.loadSettings("@openacp/speech");
19895
20969
  const newSpeechConfig = buildSpeechServiceConfig(pluginCfg);
19896
20970
  speechSvc.refreshProviders(newSpeechConfig);
19897
- log17.info("Speech service config updated at runtime (from plugin settings)");
20971
+ log18.info("Speech service config updated at runtime (from plugin settings)");
19898
20972
  }
19899
20973
  }
19900
20974
  }
@@ -19935,14 +21009,14 @@ var OpenACPCore = class {
19935
21009
  */
19936
21010
  async start() {
19937
21011
  this.agentCatalog.refreshRegistryIfStale().catch((err) => {
19938
- log17.warn({ err }, "Background registry refresh failed");
21012
+ log18.warn({ err }, "Background registry refresh failed");
19939
21013
  });
19940
21014
  const failures = [];
19941
21015
  for (const [name, adapter] of this.adapters.entries()) {
19942
21016
  try {
19943
21017
  await adapter.start();
19944
21018
  } catch (err) {
19945
- log17.error({ err, adapter: name }, `Adapter "${name}" failed to start`);
21019
+ log18.error({ err, adapter: name }, `Adapter "${name}" failed to start`);
19946
21020
  failures.push({ name, error: err });
19947
21021
  }
19948
21022
  }
@@ -20023,7 +21097,7 @@ var OpenACPCore = class {
20023
21097
  * If no session is found, the user is told to start one with /new.
20024
21098
  */
20025
21099
  async handleMessage(message, initialMeta) {
20026
- log17.debug(
21100
+ log18.debug(
20027
21101
  {
20028
21102
  channelId: message.channelId,
20029
21103
  threadId: message.threadId,
@@ -20044,7 +21118,7 @@ var OpenACPCore = class {
20044
21118
  }
20045
21119
  const access2 = await this.securityGuard.checkAccess(message);
20046
21120
  if (!access2.allowed) {
20047
- log17.warn({ userId: message.userId, reason: access2.reason }, "Access denied");
21121
+ log18.warn({ userId: message.userId, reason: access2.reason }, "Access denied");
20048
21122
  if (access2.reason.includes("Session limit")) {
20049
21123
  const adapter = this.adapters.get(message.channelId);
20050
21124
  if (adapter) {
@@ -20058,7 +21132,7 @@ var OpenACPCore = class {
20058
21132
  }
20059
21133
  let session = await this.sessionFactory.getOrResume(message.channelId, message.threadId);
20060
21134
  if (!session) {
20061
- log17.warn(
21135
+ log18.warn(
20062
21136
  { channelId: message.channelId, threadId: message.threadId },
20063
21137
  "No session found for thread (in-memory miss + lazy resume returned null)"
20064
21138
  );
@@ -20110,7 +21184,7 @@ ${text3}`;
20110
21184
  });
20111
21185
  session.enqueuePrompt(text3, attachments, routing, turnId, meta).catch((err) => {
20112
21186
  const reason = err instanceof Error ? err.message : String(err);
20113
- log17.warn({ err, sessionId: session.id, turnId, reason }, "enqueuePrompt failed \u2014 emitting message:failed");
21187
+ log18.warn({ err, sessionId: session.id, turnId, reason }, "enqueuePrompt failed \u2014 emitting message:failed");
20114
21188
  this.eventBus.emit(BusEvent.MESSAGE_FAILED, {
20115
21189
  sessionId: session.id,
20116
21190
  turnId,
@@ -20227,7 +21301,7 @@ ${text3}`;
20227
21301
  const bridge = this.createBridge(session, adapter, session.channelId);
20228
21302
  bridge.connect();
20229
21303
  adapter.flushPendingSkillCommands?.(session.id).catch((err) => {
20230
- log17.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
21304
+ log18.warn({ err, sessionId: session.id }, "Failed to flush pending skill commands");
20231
21305
  });
20232
21306
  if (params.createThread && session.threadId) {
20233
21307
  this.eventBus.emit(BusEvent.SESSION_THREAD_READY, {
@@ -20241,14 +21315,14 @@ ${text3}`;
20241
21315
  session.agentInstance.onPermissionRequest = async (permRequest) => {
20242
21316
  const allowOption = permRequest.options.find((o) => o.isAllow);
20243
21317
  if (!allowOption) {
20244
- log17.warn(
21318
+ log18.warn(
20245
21319
  { sessionId: session.id, permissionId: permRequest.id, description: permRequest.description },
20246
21320
  "Headless session has no allow option for permission request \u2014 skipping auto-approve, will time out"
20247
21321
  );
20248
21322
  return new Promise(() => {
20249
21323
  });
20250
21324
  }
20251
- log17.warn(
21325
+ log18.warn(
20252
21326
  { sessionId: session.id, permissionId: permRequest.id, option: allowOption.id },
20253
21327
  `Auto-approving permission "${permRequest.description}" for headless session \u2014 no adapter connected`
20254
21328
  );
@@ -20302,7 +21376,7 @@ ${text3}`;
20302
21376
  notificationManager: this.notificationManager,
20303
21377
  tunnelService: this._tunnelService
20304
21378
  });
20305
- log17.info(
21379
+ log18.info(
20306
21380
  { sessionId: session.id, agentName: params.agentName },
20307
21381
  "Session created via pipeline"
20308
21382
  );
@@ -20721,33 +21795,33 @@ init_doctor();
20721
21795
  init_config_registry();
20722
21796
 
20723
21797
  // src/core/config/config-editor.ts
20724
- import * as path32 from "path";
21798
+ import * as path33 from "path";
20725
21799
  import * as clack2 from "@clack/prompts";
20726
21800
 
20727
21801
  // src/cli/autostart.ts
20728
21802
  init_log();
20729
21803
  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");
21804
+ import * as fs29 from "fs";
21805
+ import * as path27 from "path";
21806
+ import * as os9 from "os";
21807
+ var log20 = createChildLogger({ module: "autostart" });
21808
+ var LEGACY_LAUNCHD_PLIST_PATH = path27.join(os9.homedir(), "Library", "LaunchAgents", "com.openacp.daemon.plist");
21809
+ var LEGACY_SYSTEMD_SERVICE_PATH = path27.join(os9.homedir(), ".config", "systemd", "user", "openacp.service");
20736
21810
  function getLaunchdLabel(instanceId) {
20737
21811
  return `com.openacp.daemon.${instanceId}`;
20738
21812
  }
20739
21813
  function getLaunchdPlistPath(instanceId) {
20740
- return path25.join(os7.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
21814
+ return path27.join(os9.homedir(), "Library", "LaunchAgents", `${getLaunchdLabel(instanceId)}.plist`);
20741
21815
  }
20742
21816
  function getSystemdServiceName(instanceId) {
20743
21817
  return `openacp-${instanceId}`;
20744
21818
  }
20745
21819
  function getSystemdServicePath(instanceId) {
20746
- return path25.join(os7.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
21820
+ return path27.join(os9.homedir(), ".config", "systemd", "user", `${getSystemdServiceName(instanceId)}.service`);
20747
21821
  }
20748
21822
  function getAutoStartState(instanceId) {
20749
21823
  if (process.platform === "linux") {
20750
- const installed = fs27.existsSync(getSystemdServicePath(instanceId));
21824
+ const installed = fs29.existsSync(getSystemdServicePath(instanceId));
20751
21825
  if (!installed) return { installed: false, manager: null, active: false };
20752
21826
  try {
20753
21827
  const output = execFileSync6("systemctl", [
@@ -20769,7 +21843,7 @@ function getAutoStartState(instanceId) {
20769
21843
  }
20770
21844
  }
20771
21845
  if (process.platform === "darwin") {
20772
- const installed = fs27.existsSync(getLaunchdPlistPath(instanceId));
21846
+ const installed = fs29.existsSync(getLaunchdPlistPath(instanceId));
20773
21847
  if (!installed) return { installed: false, manager: null, active: false };
20774
21848
  try {
20775
21849
  const uid = process.getuid();
@@ -20799,7 +21873,7 @@ function escapeSystemdValue(str) {
20799
21873
  }
20800
21874
  function generateLaunchdPlist(nodePath, cliPath, logDir2, instanceRoot, instanceId) {
20801
21875
  const label = getLaunchdLabel(instanceId);
20802
- const logFile = path25.join(logDir2, "openacp.log");
21876
+ const logFile = path27.join(logDir2, "openacp.log");
20803
21877
  return `<?xml version="1.0" encoding="UTF-8"?>
20804
21878
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
20805
21879
  <plist version="1.0">
@@ -20851,32 +21925,32 @@ WantedBy=default.target
20851
21925
  `;
20852
21926
  }
20853
21927
  function migrateLegacy() {
20854
- if (process.platform === "darwin" && fs27.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
21928
+ if (process.platform === "darwin" && fs29.existsSync(LEGACY_LAUNCHD_PLIST_PATH)) {
20855
21929
  try {
20856
21930
  const uid = process.getuid();
20857
21931
  execFileSync6("launchctl", ["bootout", `gui/${uid}`, "com.openacp.daemon"], { stdio: "pipe" });
20858
21932
  } catch {
20859
21933
  }
20860
21934
  try {
20861
- fs27.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
21935
+ fs29.unlinkSync(LEGACY_LAUNCHD_PLIST_PATH);
20862
21936
  } catch {
20863
21937
  }
20864
- log19.info("Removed legacy single-instance LaunchAgent");
21938
+ log20.info("Removed legacy single-instance LaunchAgent");
20865
21939
  }
20866
- if (process.platform === "linux" && fs27.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
21940
+ if (process.platform === "linux" && fs29.existsSync(LEGACY_SYSTEMD_SERVICE_PATH)) {
20867
21941
  try {
20868
21942
  execFileSync6("systemctl", ["--user", "disable", "openacp"], { stdio: "pipe" });
20869
21943
  } catch {
20870
21944
  }
20871
21945
  try {
20872
- fs27.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
21946
+ fs29.unlinkSync(LEGACY_SYSTEMD_SERVICE_PATH);
20873
21947
  } catch {
20874
21948
  }
20875
21949
  try {
20876
21950
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20877
21951
  } catch {
20878
21952
  }
20879
- log19.info("Removed legacy single-instance systemd service");
21953
+ log20.info("Removed legacy single-instance systemd service");
20880
21954
  }
20881
21955
  }
20882
21956
  function installAutoStart(logDir2, instanceRoot, instanceId) {
@@ -20884,26 +21958,26 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
20884
21958
  return { success: false, error: "Auto-start not supported on this platform" };
20885
21959
  }
20886
21960
  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;
21961
+ const cliPath = path27.resolve(process.argv[1]);
21962
+ const resolvedLogDir = logDir2.startsWith("~") ? path27.join(os9.homedir(), logDir2.slice(1)) : logDir2;
20889
21963
  try {
20890
21964
  migrateLegacy();
20891
21965
  if (process.platform === "darwin") {
20892
21966
  const plistPath = getLaunchdPlistPath(instanceId);
20893
21967
  const plist = generateLaunchdPlist(nodePath, cliPath, resolvedLogDir, instanceRoot, instanceId);
20894
- const dir = path25.dirname(plistPath);
20895
- fs27.mkdirSync(dir, { recursive: true });
21968
+ const dir = path27.dirname(plistPath);
21969
+ fs29.mkdirSync(dir, { recursive: true });
20896
21970
  const uid = process.getuid();
20897
21971
  const domain = `gui/${uid}`;
20898
- const previous = fs27.existsSync(plistPath) ? fs27.readFileSync(plistPath, "utf8") : void 0;
21972
+ const previous = fs29.existsSync(plistPath) ? fs29.readFileSync(plistPath, "utf8") : void 0;
20899
21973
  const previousState = getAutoStartState(instanceId);
20900
21974
  const tmp = `${plistPath}.${process.pid}.tmp`;
20901
21975
  let replaced = false;
20902
21976
  let bootedOut = false;
20903
21977
  try {
20904
- fs27.writeFileSync(tmp, plist, { mode: 384 });
21978
+ fs29.writeFileSync(tmp, plist, { mode: 384 });
20905
21979
  execFileSync6("plutil", ["-lint", tmp], { stdio: "pipe" });
20906
- fs27.renameSync(tmp, plistPath);
21980
+ fs29.renameSync(tmp, plistPath);
20907
21981
  replaced = true;
20908
21982
  if (previousState.active) {
20909
21983
  execFileSync6("launchctl", ["bootout", domain, plistPath], { stdio: "pipe" });
@@ -20912,7 +21986,7 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
20912
21986
  execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
20913
21987
  } catch (error) {
20914
21988
  try {
20915
- fs27.rmSync(tmp, { force: true });
21989
+ fs29.rmSync(tmp, { force: true });
20916
21990
  } catch {
20917
21991
  }
20918
21992
  if (replaced) {
@@ -20922,48 +21996,48 @@ function installAutoStart(logDir2, instanceRoot, instanceId) {
20922
21996
  rollbackBootedOut = true;
20923
21997
  } catch {
20924
21998
  }
20925
- if (previous === void 0) fs27.rmSync(plistPath, { force: true });
21999
+ if (previous === void 0) fs29.rmSync(plistPath, { force: true });
20926
22000
  else {
20927
22001
  const rollbackTmp = `${plistPath}.${process.pid}.rollback.tmp`;
20928
- fs27.writeFileSync(rollbackTmp, previous, { mode: 384 });
20929
- fs27.renameSync(rollbackTmp, plistPath);
22002
+ fs29.writeFileSync(rollbackTmp, previous, { mode: 384 });
22003
+ fs29.renameSync(rollbackTmp, plistPath);
20930
22004
  if (previousState.active || bootedOut || rollbackBootedOut) execFileSync6("launchctl", ["bootstrap", domain, plistPath], { stdio: "pipe" });
20931
22005
  }
20932
22006
  }
20933
22007
  throw error;
20934
22008
  }
20935
- log19.info({ instanceId }, "LaunchAgent installed");
22009
+ log20.info({ instanceId }, "LaunchAgent installed");
20936
22010
  return { success: true };
20937
22011
  }
20938
22012
  if (process.platform === "linux") {
20939
22013
  const servicePath = getSystemdServicePath(instanceId);
20940
22014
  const serviceName = getSystemdServiceName(instanceId);
20941
22015
  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;
22016
+ const dir = path27.dirname(servicePath);
22017
+ fs29.mkdirSync(dir, { recursive: true });
22018
+ const previous = fs29.existsSync(servicePath) ? fs29.readFileSync(servicePath, "utf8") : void 0;
20945
22019
  const tmp = `${servicePath}.${process.pid}.tmp`;
20946
- fs27.writeFileSync(tmp, unit, { mode: 384 });
20947
- fs27.renameSync(tmp, servicePath);
22020
+ fs29.writeFileSync(tmp, unit, { mode: 384 });
22021
+ fs29.renameSync(tmp, servicePath);
20948
22022
  try {
20949
22023
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20950
22024
  execFileSync6("systemctl", ["--user", "enable", serviceName], { stdio: "pipe" });
20951
22025
  } catch (error) {
20952
- if (previous === void 0) fs27.unlinkSync(servicePath);
20953
- else fs27.writeFileSync(servicePath, previous);
22026
+ if (previous === void 0) fs29.unlinkSync(servicePath);
22027
+ else fs29.writeFileSync(servicePath, previous);
20954
22028
  try {
20955
22029
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20956
22030
  } catch {
20957
22031
  }
20958
22032
  throw error;
20959
22033
  }
20960
- log19.info({ instanceId }, "systemd user service installed");
22034
+ log20.info({ instanceId }, "systemd user service installed");
20961
22035
  return { success: true };
20962
22036
  }
20963
22037
  return { success: false, error: "Unsupported platform" };
20964
22038
  } catch (e) {
20965
22039
  const msg = e.message;
20966
- log19.error({ err: msg }, "Failed to install auto-start");
22040
+ log20.error({ err: msg }, "Failed to install auto-start");
20967
22041
  return { success: false, error: msg };
20968
22042
  }
20969
22043
  }
@@ -20974,44 +22048,44 @@ function uninstallAutoStart(instanceId) {
20974
22048
  try {
20975
22049
  if (process.platform === "darwin") {
20976
22050
  const plistPath = getLaunchdPlistPath(instanceId);
20977
- if (fs27.existsSync(plistPath)) {
22051
+ if (fs29.existsSync(plistPath)) {
20978
22052
  const uid = process.getuid();
20979
22053
  try {
20980
22054
  execFileSync6("launchctl", ["bootout", `gui/${uid}`, plistPath], { stdio: "pipe" });
20981
22055
  } catch {
20982
22056
  }
20983
- fs27.unlinkSync(plistPath);
20984
- log19.info({ instanceId }, "LaunchAgent removed");
22057
+ fs29.unlinkSync(plistPath);
22058
+ log20.info({ instanceId }, "LaunchAgent removed");
20985
22059
  }
20986
22060
  return { success: true };
20987
22061
  }
20988
22062
  if (process.platform === "linux") {
20989
22063
  const servicePath = getSystemdServicePath(instanceId);
20990
22064
  const serviceName = getSystemdServiceName(instanceId);
20991
- if (fs27.existsSync(servicePath)) {
22065
+ if (fs29.existsSync(servicePath)) {
20992
22066
  try {
20993
22067
  execFileSync6("systemctl", ["--user", "disable", serviceName], { stdio: "pipe" });
20994
22068
  } catch {
20995
22069
  }
20996
- fs27.unlinkSync(servicePath);
22070
+ fs29.unlinkSync(servicePath);
20997
22071
  execFileSync6("systemctl", ["--user", "daemon-reload"], { stdio: "pipe" });
20998
- log19.info({ instanceId }, "systemd user service removed");
22072
+ log20.info({ instanceId }, "systemd user service removed");
20999
22073
  }
21000
22074
  return { success: true };
21001
22075
  }
21002
22076
  return { success: false, error: "Unsupported platform" };
21003
22077
  } catch (e) {
21004
22078
  const msg = e.message;
21005
- log19.error({ err: msg }, "Failed to uninstall auto-start");
22079
+ log20.error({ err: msg }, "Failed to uninstall auto-start");
21006
22080
  return { success: false, error: msg };
21007
22081
  }
21008
22082
  }
21009
22083
  function isAutoStartInstalled(instanceId) {
21010
22084
  if (process.platform === "darwin") {
21011
- return fs27.existsSync(getLaunchdPlistPath(instanceId));
22085
+ return fs29.existsSync(getLaunchdPlistPath(instanceId));
21012
22086
  }
21013
22087
  if (process.platform === "linux") {
21014
- return fs27.existsSync(getSystemdServicePath(instanceId));
22088
+ return fs29.existsSync(getSystemdServicePath(instanceId));
21015
22089
  }
21016
22090
  return false;
21017
22091
  }
@@ -21020,25 +22094,25 @@ function isAutoStartInstalled(instanceId) {
21020
22094
  init_instance_context();
21021
22095
  init_instance_registry();
21022
22096
  init_log();
21023
- import fs30 from "fs";
21024
- import path28 from "path";
21025
- var log20 = createChildLogger({ module: "resolve-instance-id" });
22097
+ import fs31 from "fs";
22098
+ import path29 from "path";
22099
+ var log21 = createChildLogger({ module: "resolve-instance-id" });
21026
22100
  function resolveInstanceId(instanceRoot) {
21027
22101
  try {
21028
- const configPath = path28.join(instanceRoot, "config.json");
21029
- const raw = JSON.parse(fs30.readFileSync(configPath, "utf-8"));
22102
+ const configPath = path29.join(instanceRoot, "config.json");
22103
+ const raw = JSON.parse(fs31.readFileSync(configPath, "utf-8"));
21030
22104
  if (raw.id && typeof raw.id === "string") return raw.id;
21031
22105
  } catch {
21032
22106
  }
21033
22107
  try {
21034
- const reg = new InstanceRegistry(path28.join(getGlobalRoot(), "instances.json"));
22108
+ const reg = new InstanceRegistry(path29.join(getGlobalRoot(), "instances.json"));
21035
22109
  reg.load();
21036
22110
  const entry = reg.getByRoot(instanceRoot);
21037
22111
  if (entry?.id) return entry.id;
21038
22112
  } catch (err) {
21039
- log20.debug({ err: err.message, instanceRoot }, "Could not read instance registry, using fallback id");
22113
+ log21.debug({ err: err.message, instanceRoot }, "Could not read instance registry, using fallback id");
21040
22114
  }
21041
- return path28.basename(path28.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
22115
+ return path29.basename(path29.dirname(instanceRoot)).replace(/[^a-zA-Z0-9-]/g, "-") || "default";
21042
22116
  }
21043
22117
 
21044
22118
  // src/core/config/config-editor.ts
@@ -21610,7 +22684,7 @@ async function runConfigEditor(configManager, mode = "file", apiPort, settingsMa
21610
22684
  await configManager.load();
21611
22685
  const config = configManager.get();
21612
22686
  const updates = {};
21613
- const instanceRoot = path32.dirname(configManager.getConfigPath());
22687
+ const instanceRoot = path33.dirname(configManager.getConfigPath());
21614
22688
  console.log(`
21615
22689
  ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
21616
22690
  console.log(dim(`Config: ${configManager.getConfigPath()}`));
@@ -21667,17 +22741,17 @@ ${c.cyan}${c.bold}OpenACP Config Editor${c.reset}`);
21667
22741
  async function sendConfigViaApi(port, updates) {
21668
22742
  const { apiCall: call } = await Promise.resolve().then(() => (init_api_client(), api_client_exports));
21669
22743
  const paths = flattenToPaths(updates);
21670
- for (const { path: path37, value } of paths) {
22744
+ for (const { path: path38, value } of paths) {
21671
22745
  const res = await call(port, "/api/config", {
21672
22746
  method: "PATCH",
21673
22747
  headers: { "Content-Type": "application/json" },
21674
- body: JSON.stringify({ path: path37, value })
22748
+ body: JSON.stringify({ path: path38, value })
21675
22749
  });
21676
22750
  const data = await res.json();
21677
22751
  if (!res.ok) {
21678
- console.log(warn(`Failed to update ${path37}: ${data.error}`));
22752
+ console.log(warn(`Failed to update ${path38}: ${data.error}`));
21679
22753
  } else if (data.needsRestart) {
21680
- console.log(warn(`${path37} updated \u2014 restart required`));
22754
+ console.log(warn(`${path38} updated \u2014 restart required`));
21681
22755
  }
21682
22756
  }
21683
22757
  }
@@ -21697,25 +22771,25 @@ function flattenToPaths(obj, prefix = "") {
21697
22771
  // src/cli/daemon.ts
21698
22772
  init_config();
21699
22773
  import { spawn as spawn3 } from "child_process";
21700
- import * as fs33 from "fs";
21701
- import * as path33 from "path";
22774
+ import * as fs34 from "fs";
22775
+ import * as path34 from "path";
21702
22776
  function getPidPath(root) {
21703
- return path33.join(root, "openacp.pid");
22777
+ return path34.join(root, "openacp.pid");
21704
22778
  }
21705
22779
  function getLogDir(root) {
21706
- return path33.join(root, "logs");
22780
+ return path34.join(root, "logs");
21707
22781
  }
21708
22782
  function getRunningMarker(root) {
21709
- return path33.join(root, "running");
22783
+ return path34.join(root, "running");
21710
22784
  }
21711
22785
  function writePidFile(pidPath, pid) {
21712
- const dir = path33.dirname(pidPath);
21713
- fs33.mkdirSync(dir, { recursive: true });
21714
- fs33.writeFileSync(pidPath, String(pid));
22786
+ const dir = path34.dirname(pidPath);
22787
+ fs34.mkdirSync(dir, { recursive: true });
22788
+ fs34.writeFileSync(pidPath, String(pid));
21715
22789
  }
21716
22790
  function readPidFile(pidPath) {
21717
22791
  try {
21718
- const content = fs33.readFileSync(pidPath, "utf-8").trim();
22792
+ const content = fs34.readFileSync(pidPath, "utf-8").trim();
21719
22793
  const pid = parseInt(content, 10);
21720
22794
  return isNaN(pid) ? null : pid;
21721
22795
  } catch {
@@ -21724,7 +22798,7 @@ function readPidFile(pidPath) {
21724
22798
  }
21725
22799
  function removePidFile(pidPath) {
21726
22800
  try {
21727
- fs33.unlinkSync(pidPath);
22801
+ fs34.unlinkSync(pidPath);
21728
22802
  } catch {
21729
22803
  }
21730
22804
  }
@@ -21757,12 +22831,12 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
21757
22831
  return { error: `Already running (PID ${pid})` };
21758
22832
  }
21759
22833
  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]);
22834
+ fs34.mkdirSync(resolvedLogDir, { recursive: true });
22835
+ const logFile = path34.join(resolvedLogDir, "openacp.log");
22836
+ const cliPath = path34.resolve(process.argv[1]);
21763
22837
  const nodePath = process.execPath;
21764
- const out = fs33.openSync(logFile, "a");
21765
- const err = fs33.openSync(logFile, "a");
22838
+ const out = fs34.openSync(logFile, "a");
22839
+ const err = fs34.openSync(logFile, "a");
21766
22840
  const child = spawn3(nodePath, [cliPath, "--daemon-child"], {
21767
22841
  detached: true,
21768
22842
  stdio: ["ignore", out, err],
@@ -21771,8 +22845,8 @@ function startDaemon(pidPath, logDir2, instanceRoot) {
21771
22845
  ...instanceRoot ? { OPENACP_INSTANCE_ROOT: instanceRoot } : {}
21772
22846
  }
21773
22847
  });
21774
- fs33.closeSync(out);
21775
- fs33.closeSync(err);
22848
+ fs34.closeSync(out);
22849
+ fs34.closeSync(err);
21776
22850
  if (!child.pid) {
21777
22851
  return { error: "Failed to spawn daemon process" };
21778
22852
  }
@@ -21843,12 +22917,12 @@ async function stopDaemon(pidPath, instanceRoot) {
21843
22917
  }
21844
22918
  function markRunning(root) {
21845
22919
  const marker = getRunningMarker(root);
21846
- fs33.mkdirSync(path33.dirname(marker), { recursive: true });
21847
- fs33.writeFileSync(marker, "");
22920
+ fs34.mkdirSync(path34.dirname(marker), { recursive: true });
22921
+ fs34.writeFileSync(marker, "");
21848
22922
  }
21849
22923
  function clearRunning(root) {
21850
22924
  try {
21851
- fs33.unlinkSync(getRunningMarker(root));
22925
+ fs34.unlinkSync(getRunningMarker(root));
21852
22926
  } catch {
21853
22927
  }
21854
22928
  }
@@ -21864,7 +22938,7 @@ init_static_server();
21864
22938
 
21865
22939
  // src/plugins/telegram/topic-manager.ts
21866
22940
  init_log();
21867
- var log23 = createChildLogger({ module: "topic-manager" });
22941
+ var log24 = createChildLogger({ module: "topic-manager" });
21868
22942
  var TopicManager = class {
21869
22943
  constructor(sessionManager, adapter, systemTopicIds) {
21870
22944
  this.sessionManager = sessionManager;
@@ -21916,7 +22990,7 @@ var TopicManager = class {
21916
22990
  try {
21917
22991
  await this.adapter.deleteSessionThread?.(sessionId);
21918
22992
  } catch (err) {
21919
- log23.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
22993
+ log24.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
21920
22994
  }
21921
22995
  }
21922
22996
  await this.sessionManager.removeRecord(sessionId);
@@ -21943,7 +23017,7 @@ var TopicManager = class {
21943
23017
  try {
21944
23018
  await this.adapter.deleteSessionThread?.(record.sessionId);
21945
23019
  } catch (err) {
21946
- log23.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
23020
+ log24.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
21947
23021
  }
21948
23022
  }
21949
23023
  await this.sessionManager.removeRecord(record.sessionId);