// Force strict mode and setup for ESM
"use strict";
import {
  ClientSideConnection,
  PROTOCOL_VERSION,
  ndJsonStream
} from "./chunk-UFC57OYT.js";
import {
  init_esbuild_shims
} from "./chunk-A4BMJM77.js";
import {
  __name
} from "./chunk-J2S4EL5Y.js";

// packages/channels/base/dist/sanitize.js
init_esbuild_shims();
var PROMPT_UNSAFE_INVISIBLES = /[\u0080-\u009f\u200b-\u200d\u2028\u2029\u202a-\u202e\u2060\u2066-\u2069\ufeff]/g;
function truncateCodePoints(str, max) {
  const cp = Array.from(str);
  return cp.length > max ? cp.slice(0, max).join("") : str;
}
__name(truncateCodePoints, "truncateCodePoints");
function sanitizeSenderName(name) {
  const cleaned = name.replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/[[\]\r\n]/g, " ");
  return truncateCodePoints(cleaned, 64).trim() || "unknown";
}
__name(sanitizeSenderName, "sanitizeSenderName");
function sanitizeQuotedText(text, maxLen) {
  const cleaned = text.replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/["[\]]/g, " ");
  const cp = Array.from(cleaned);
  return cp.length > maxLen ? cp.slice(0, maxLen - 1).join("") + "\u2026" : cleaned;
}
__name(sanitizeQuotedText, "sanitizeQuotedText");
function sanitizePromptText(text) {
  return text.replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/^([ \t]*)\[([^\]\r\n]{1,64})\](:?)/gm, "$1$2$3").replace(/[\u0000-\u001f\u007f]/g, " ");
}
__name(sanitizePromptText, "sanitizePromptText");
function sanitizePromptPath(path4) {
  const cleaned = path4.replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/[\u0000-\u001f\u007f]/g, " ");
  return truncateCodePoints(cleaned, 1024);
}
__name(sanitizePromptPath, "sanitizePromptPath");
function sanitizeLogText(text, maxLen) {
  return truncateCodePoints(text, maxLen).replace(/\n/g, "\\n").replace(PROMPT_UNSAFE_INVISIBLES, " ").replace(/[\u0000-\u001f\u007f]/g, " ");
}
__name(sanitizeLogText, "sanitizeLogText");

// packages/channels/base/dist/index.js
init_esbuild_shims();

// packages/channels/base/dist/paths.js
init_esbuild_shims();
import * as path from "node:path";
import * as os from "node:os";
function resolvePath(dir) {
  let resolved = dir;
  if (resolved === "~" || resolved.startsWith("~/") || resolved.startsWith("~\\")) {
    const relativeSegments = resolved === "~" ? [] : resolved.slice(2).split(/[/\\]+/).filter(Boolean);
    resolved = path.join(os.homedir(), ...relativeSegments);
  }
  if (!path.isAbsolute(resolved)) {
    resolved = path.resolve(resolved);
  }
  return resolved;
}
__name(resolvePath, "resolvePath");
function getGlobalQwenDir() {
  const envDir = process.env["QWEN_HOME"];
  if (envDir) {
    return resolvePath(envDir);
  }
  const homeDir = os.homedir();
  return homeDir ? path.join(homeDir, ".qwen") : path.join(os.tmpdir(), ".qwen");
}
__name(getGlobalQwenDir, "getGlobalQwenDir");

// packages/channels/base/dist/AcpBridge.js
init_esbuild_shims();
import { spawn } from "node:child_process";
import { Readable, Writable } from "node:stream";
import { EventEmitter } from "node:events";
function readAvailableCommandAltNames(raw) {
  if (typeof raw !== "object" || raw === null)
    return void 0;
  const record = raw;
  const meta = record["_meta"];
  const fromMeta = typeof meta === "object" && meta !== null ? meta["altNames"] : void 0;
  const source = Array.isArray(record["altNames"]) ? record["altNames"] : Array.isArray(fromMeta) ? fromMeta : void 0;
  if (!source)
    return void 0;
  const names = source.filter((n) => typeof n === "string");
  return names.length > 0 ? names : void 0;
}
__name(readAvailableCommandAltNames, "readAvailableCommandAltNames");
var AcpBridge = class extends EventEmitter {
  static {
    __name(this, "AcpBridge");
  }
  child = null;
  connection = null;
  options;
  _availableCommands = [];
  constructor(options) {
    super();
    this.options = options;
  }
  get availableCommands() {
    return this._availableCommands;
  }
  async start() {
    const { cliEntryPath, cwd } = this.options;
    const args = [
      ...process.execArgv.filter((a) => !/^--inspect(-brk)?($|=)/.test(a)),
      cliEntryPath,
      "--acp"
    ];
    if (this.options.model) {
      args.push("--model", this.options.model);
    }
    this.child = spawn(process.execPath, args, {
      cwd,
      stdio: ["pipe", "pipe", "pipe"],
      env: { ...process.env },
      shell: false
    });
    this.child.stderr?.on("data", (data) => {
      const msg = data.toString().trim();
      if (msg) {
        process.stderr.write(`[AcpBridge] ${msg}
`);
      }
    });
    this.child.on("exit", (code, signal) => {
      process.stderr.write(`[AcpBridge] Process exited (code=${code}, signal=${signal})
`);
      this.connection = null;
      this.child = null;
      this.emit("disconnected", code, signal);
    });
    await new Promise((resolve2) => setTimeout(resolve2, 1e3));
    if (!this.child || this.child.killed) {
      throw new Error("ACP process failed to start");
    }
    const stdout = Readable.toWeb(this.child.stdout);
    const stdin = Writable.toWeb(this.child.stdin);
    const stream = ndJsonStream(stdin, stdout);
    this.connection = new ClientSideConnection(() => ({
      sessionUpdate: /* @__PURE__ */ __name((params) => {
        this.handleSessionUpdate(params);
        return Promise.resolve();
      }, "sessionUpdate"),
      requestPermission: /* @__PURE__ */ __name(async (params) => {
        const options = Array.isArray(params.options) ? params.options : [];
        const optionId = options.find((o) => o.optionId === "proceed_once")?.optionId || options[0]?.optionId || "proceed_once";
        return { outcome: { outcome: "selected", optionId } };
      }, "requestPermission"),
      extNotification: /* @__PURE__ */ __name(async () => {
      }, "extNotification")
    }), stream);
    await this.connection.initialize({
      protocolVersion: PROTOCOL_VERSION,
      clientCapabilities: {}
    });
  }
  async newSession(cwd) {
    const conn = this.ensureConnection();
    const response = await conn.newSession({ cwd, mcpServers: [] });
    return response.sessionId;
  }
  async loadSession(sessionId, cwd) {
    const conn = this.ensureConnection();
    const response = await conn.loadSession({
      sessionId,
      cwd,
      mcpServers: []
    });
    return response.sessionId;
  }
  async prompt(sessionId, text, options) {
    const conn = this.ensureConnection();
    const chunks = [];
    const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
      if (sid === sessionId)
        chunks.push(chunk);
    }, "onChunk");
    this.on("textChunk", onChunk);
    const prompt = [];
    if (options?.imageBase64 && options.imageMimeType) {
      prompt.push({
        type: "image",
        data: options.imageBase64,
        mimeType: options.imageMimeType
      });
    }
    prompt.push({ type: "text", text });
    try {
      await conn.prompt({
        sessionId,
        prompt
      });
    } finally {
      this.off("textChunk", onChunk);
    }
    return chunks.join("");
  }
  async cancelSession(sessionId) {
    const conn = this.ensureConnection();
    await conn.cancel({ sessionId });
  }
  stop() {
    if (this.child) {
      this.child.kill();
      this.child = null;
    }
    this.connection = null;
  }
  get isConnected() {
    return this.child !== null && !this.child.killed && this.child.exitCode === null;
  }
  handleSessionUpdate(params) {
    const { sessionId } = params;
    const update = params["update"];
    if (!update)
      return;
    const type = update["sessionUpdate"];
    switch (type) {
      case "agent_message_chunk": {
        const content = update["content"];
        if (content?.type === "text" && content.text) {
          this.emit("textChunk", sessionId, content.text);
        }
        break;
      }
      case "tool_call": {
        const event = {
          sessionId,
          toolCallId: update["toolCallId"],
          kind: update["kind"] || "",
          title: update["title"] || "",
          status: update["status"] || "pending",
          rawInput: update["rawInput"]
        };
        this.emit("toolCall", event);
        break;
      }
      case "available_commands_update": {
        if (Array.isArray(update["availableCommands"])) {
          this._availableCommands = update["availableCommands"].map((cmd) => {
            const altNames = readAvailableCommandAltNames(cmd);
            return altNames ? { ...cmd, altNames } : cmd;
          });
        }
        break;
      }
      default:
        break;
    }
    this.emit("sessionUpdate", params);
  }
  ensureConnection() {
    if (!this.connection || !this.isConnected) {
      throw new Error("Not connected to ACP agent");
    }
    return this.connection;
  }
};

// packages/channels/base/dist/DaemonChannelBridge.js
init_esbuild_shims();
import { EventEmitter as EventEmitter2 } from "node:events";
var MAX_RESPONDED_PERMISSION_REQUESTS = 256;
function isRecord(value) {
  return typeof value === "object" && value !== null;
}
__name(isRecord, "isRecord");
function getString(value) {
  return typeof value === "string" ? value : void 0;
}
__name(getString, "getString");
function getTextContent(content) {
  if (!isRecord(content)) {
    return void 0;
  }
  return getString(content["text"]);
}
__name(getTextContent, "getTextContent");
function getSessionUpdate(data) {
  if (!isRecord(data) || !isRecord(data["update"])) {
    return void 0;
  }
  return data["update"];
}
__name(getSessionUpdate, "getSessionUpdate");
function isAvailableCommand(value) {
  if (!isRecord(value) || typeof value["name"] !== "string")
    return false;
  const altNames = value["altNames"];
  return altNames === void 0 || Array.isArray(altNames) && altNames.every((n) => typeof n === "string");
}
__name(isAvailableCommand, "isAvailableCommand");
function isPermissionRequestData(value) {
  if (!isRecord(value) || typeof value["requestId"] !== "string" || !isRecord(value["toolCall"]) || typeof value["toolCall"]["toolCallId"] !== "string" || typeof value["toolCall"]["kind"] !== "string" || !Array.isArray(value["options"])) {
    return false;
  }
  return value["options"].every((option) => isRecord(option) && typeof option["optionId"] === "string");
}
__name(isPermissionRequestData, "isPermissionRequestData");
function parsePermissionOutcome(value) {
  if (!isRecord(value)) {
    return void 0;
  }
  if (value["outcome"] === "cancelled") {
    return { outcome: "cancelled" };
  }
  if (value["outcome"] === "selected" && typeof value["optionId"] === "string") {
    return { outcome: "selected", optionId: value["optionId"] };
  }
  return void 0;
}
__name(parsePermissionOutcome, "parsePermissionOutcome");
function summarizeProtocolDetails(details) {
  if (!isRecord(details)) {
    return { type: typeof details };
  }
  const summary = {};
  for (const key of [
    "requestId",
    "sessionId",
    "sessionUpdate",
    "modelId",
    "requestedModelId",
    "toolCallId",
    "kind"
  ]) {
    const value = details[key];
    if (typeof value === "string") {
      summary[key] = value;
    }
  }
  return summary;
}
__name(summarizeProtocolDetails, "summarizeProtocolDetails");
async function drainDaemonEventLoop() {
  await new Promise((resolve2) => setTimeout(resolve2, 0));
}
__name(drainDaemonEventLoop, "drainDaemonEventLoop");
var DaemonChannelBridge = class extends EventEmitter2 {
  static {
    __name(this, "DaemonChannelBridge");
  }
  options;
  sessions = /* @__PURE__ */ new Map();
  eventControllers = /* @__PURE__ */ new Map();
  requestToSession = /* @__PURE__ */ new Map();
  respondedRequestToSession = /* @__PURE__ */ new Map();
  activePrompts = /* @__PURE__ */ new Set();
  activePromptControllers = /* @__PURE__ */ new Map();
  availableCommandsBySession = /* @__PURE__ */ new Map();
  connected = false;
  latestAvailableCommandsSessionId;
  lastError;
  constructor(options) {
    super();
    this.options = options;
    this.on("error", (error) => {
      this.lastError = error;
    });
  }
  get availableCommands() {
    if (this.latestAvailableCommandsSessionId) {
      return this.availableCommandsBySession.get(this.latestAvailableCommandsSessionId) ?? [];
    }
    return Array.from(this.availableCommandsBySession.values()).at(-1) ?? [];
  }
  get lastDaemonError() {
    return this.lastError;
  }
  getAvailableCommands(sessionId) {
    return this.availableCommandsBySession.get(sessionId) ?? [];
  }
  async start() {
    this.connected = true;
  }
  async newSession(cwd) {
    const session = await this.options.sessionFactory({
      workspaceCwd: cwd || this.options.cwd,
      modelServiceId: this.options.modelServiceId,
      sessionScope: this.options.sessionScope ?? "thread"
    });
    this.attachSession(session);
    return session.sessionId;
  }
  async loadSession(sessionId, cwd) {
    const session = await this.options.sessionFactory({
      workspaceCwd: cwd || this.options.cwd,
      modelServiceId: this.options.modelServiceId,
      sessionId,
      sessionScope: this.options.sessionScope ?? "thread"
    });
    if (session.sessionId !== sessionId) {
      throw new Error(`Daemon returned session ${session.sessionId} while loading ${sessionId}`);
    }
    this.attachSession(session);
    return session.sessionId;
  }
  async prompt(sessionId, text, options) {
    const session = this.ensureSession(sessionId);
    if (this.activePrompts.has(sessionId)) {
      throw new Error(`Prompt already in flight for daemon session ${sessionId}`);
    }
    this.activePrompts.add(sessionId);
    const controller = new AbortController();
    let controllers = this.activePromptControllers.get(sessionId);
    if (!controllers) {
      controllers = /* @__PURE__ */ new Set();
      this.activePromptControllers.set(sessionId, controllers);
    }
    controllers.add(controller);
    const chunks = [];
    const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
      if (sid === sessionId) {
        chunks.push(chunk);
      }
    }, "onChunk");
    const onSessionDied = /* @__PURE__ */ __name((info) => {
      if (info.sessionId === sessionId) {
        controller.abort();
      }
    }, "onSessionDied");
    this.on("textChunk", onChunk);
    this.on("sessionDied", onSessionDied);
    const prompt = [];
    if (options?.imageBase64 && options.imageMimeType) {
      prompt.push({
        type: "image",
        data: options.imageBase64,
        mimeType: options.imageMimeType
      });
    }
    prompt.push({ type: "text", text });
    try {
      const result = await session.prompt({ prompt }, controller.signal);
      await drainDaemonEventLoop();
      const textResult = chunks.join("");
      this.emit("promptComplete", {
        sessionId,
        text: textResult,
        stopReason: result.stopReason
      });
      return textResult;
    } finally {
      this.off("textChunk", onChunk);
      this.off("sessionDied", onSessionDied);
      this.activePrompts.delete(sessionId);
      controllers.delete(controller);
      if (controllers.size === 0 && this.activePromptControllers.get(sessionId) === controllers) {
        this.activePromptControllers.delete(sessionId);
      }
    }
  }
  async shellCommand(sessionId, command, signal) {
    const session = this.ensureSession(sessionId);
    if (!session.shellCommand) {
      throw new Error("Shell command not supported by this session client");
    }
    return session.shellCommand(command, signal);
  }
  async cancelSession(sessionId) {
    const session = this.ensureSession(sessionId);
    await session.cancel();
    this.abortActivePrompts(sessionId);
    this.activePrompts.delete(sessionId);
  }
  async setSessionModel(sessionId, modelId) {
    return await this.ensureSession(sessionId).setModel(modelId);
  }
  async respondToPermission(requestId, response) {
    const sessionId = this.requestToSession.get(requestId);
    if (!sessionId) {
      return false;
    }
    const session = this.sessions.get(sessionId);
    if (!session) {
      this.requestToSession.delete(requestId);
      this.respondedRequestToSession.delete(requestId);
      return false;
    }
    try {
      const accepted = await session.respondToPermission(requestId, response);
      this.requestToSession.delete(requestId);
      if (accepted) {
        this.rememberRespondedPermissionRequest(requestId, sessionId);
      } else {
        this.respondedRequestToSession.delete(requestId);
      }
      return accepted;
    } catch (error) {
      this.requestToSession.delete(requestId);
      this.respondedRequestToSession.delete(requestId);
      throw error;
    }
  }
  stop() {
    for (const sessionId of Array.from(this.sessions.keys())) {
      const session = this.sessions.get(sessionId);
      if (session) {
        void session.cancel().catch((error) => {
          this.lastError = error;
        });
      }
      this.dropSession(sessionId, "bridge_stopped");
    }
    this.latestAvailableCommandsSessionId = void 0;
    this.connected = false;
  }
  get isConnected() {
    return this.connected;
  }
  attachSession(session) {
    if (this.sessions.has(session.sessionId)) {
      this.dropSession(session.sessionId, "session_replaced");
    }
    this.sessions.set(session.sessionId, session);
    const controller = new AbortController();
    this.eventControllers.set(session.sessionId, controller);
    void this.pumpEvents(session, controller.signal);
  }
  ensureSession(sessionId) {
    const session = this.sessions.get(sessionId);
    if (!session) {
      throw new Error(`No daemon session bound for ${sessionId}`);
    }
    return session;
  }
  async pumpEvents(session, signal) {
    try {
      for await (const event of session.events({
        signal,
        lastEventId: session.lastEventId,
        resume: true
      })) {
        if (!this.isCurrentPump(session, signal)) {
          return;
        }
        this.handleEvent(session, event);
      }
      if (!signal.aborted && this.isCurrentPump(session, signal)) {
        this.dropSession(session.sessionId, "stream_ended");
      }
    } catch (error) {
      if (!signal.aborted && this.isCurrentPump(session, signal)) {
        this.emit("error", error);
        this.dropSession(session.sessionId, error instanceof Error ? error.message : String(error));
      }
    }
  }
  isCurrentPump(session, signal) {
    return this.sessions.get(session.sessionId) === session && this.eventControllers.get(session.sessionId)?.signal === signal;
  }
  handleEvent(session, event) {
    switch (event.type) {
      case "session_update":
        this.handleSessionUpdate(session.sessionId, event.data);
        break;
      case "permission_request":
        this.handlePermissionRequest(session.sessionId, event.data);
        break;
      case "permission_resolved":
        this.handlePermissionResolved(session.sessionId, event.data);
        break;
      case "model_switched":
        this.handleModelSwitched(session.sessionId, event.data);
        break;
      case "model_switch_failed":
        this.handleModelSwitchFailed(session.sessionId, event.data);
        break;
      case "session_died":
        this.handleSessionDied(session.sessionId, event.data);
        break;
      case "client_evicted":
        this.dropSession(session.sessionId, this.getStringField(event.data, "reason", "client_evicted"));
        break;
      case "stream_error":
        this.dropSession(session.sessionId, this.getStringField(event.data, "error", "stream_error"));
        break;
      default:
        break;
    }
  }
  handleSessionUpdate(sessionId, data) {
    const update = getSessionUpdate(data);
    if (!update) {
      this.emitProtocolError("Malformed daemon session_update event", data);
      return;
    }
    const type = getString(update["sessionUpdate"]);
    switch (type) {
      case "agent_message_chunk": {
        const text = getTextContent(update["content"]);
        if (text) {
          this.emit("textChunk", sessionId, text);
        }
        break;
      }
      case "agent_thought_chunk": {
        const text = getTextContent(update["content"]);
        if (text) {
          this.emit("thoughtChunk", sessionId, text);
        }
        break;
      }
      case "tool_call":
      case "tool_call_update": {
        const toolCallId = getString(update["toolCallId"]);
        const kind = getString(update["kind"]);
        if (!toolCallId || !kind) {
          this.emitProtocolError(`Malformed daemon ${type} event`, update);
          break;
        }
        const event = {
          sessionId,
          toolCallId,
          kind,
          title: getString(update["title"]) ?? "",
          status: getString(update["status"]) ?? "pending",
          rawInput: isRecord(update["rawInput"]) ? update["rawInput"] : void 0
        };
        this.emit("toolCall", event);
        break;
      }
      case "available_commands_update": {
        if (Array.isArray(update["availableCommands"])) {
          const commands = update["availableCommands"].filter(isAvailableCommand).map((cmd) => {
            const altNames = readAvailableCommandAltNames(cmd);
            return altNames ? { ...cmd, altNames } : cmd;
          });
          this.availableCommandsBySession.set(sessionId, commands);
          this.latestAvailableCommandsSessionId = sessionId;
        } else {
          this.emitProtocolError("Malformed daemon available_commands_update event", data);
        }
        break;
      }
      default:
        break;
    }
    this.emit("sessionUpdate", data);
  }
  handlePermissionRequest(sessionId, data) {
    if (!isPermissionRequestData(data)) {
      this.emitProtocolError("Malformed daemon permission_request event", data);
      return;
    }
    const requestId = data["requestId"];
    this.requestToSession.set(requestId, sessionId);
    this.emit("permissionRequest", {
      requestId,
      sessionId,
      request: data
    });
  }
  rememberRespondedPermissionRequest(requestId, sessionId) {
    this.respondedRequestToSession.set(requestId, sessionId);
    while (this.respondedRequestToSession.size > MAX_RESPONDED_PERMISSION_REQUESTS) {
      const oldestRequestId = this.respondedRequestToSession.keys().next().value;
      if (oldestRequestId === void 0) {
        return;
      }
      this.respondedRequestToSession.delete(oldestRequestId);
    }
  }
  handlePermissionResolved(sessionId, data) {
    if (!isRecord(data) || typeof data["requestId"] !== "string") {
      this.emitProtocolError("Malformed daemon permission_resolved event", data);
      return;
    }
    const requestId = data["requestId"];
    const mappedSessionId = this.requestToSession.get(requestId) ?? this.respondedRequestToSession.get(requestId);
    if (!mappedSessionId) {
      this.emitProtocolError(`Ignoring daemon permission_resolved for unknown request ${requestId}`, data);
      return;
    }
    if (mappedSessionId !== sessionId) {
      this.requestToSession.delete(requestId);
      this.respondedRequestToSession.delete(requestId);
      this.emitProtocolError(`Ignoring daemon permission_resolved for request ${requestId} from non-owning session ${sessionId}`, data);
      return;
    }
    const outcome = parsePermissionOutcome(data["outcome"]);
    if (!outcome) {
      this.requestToSession.delete(requestId);
      this.respondedRequestToSession.delete(requestId);
      this.emitProtocolError("Malformed daemon permission_resolved outcome", data);
      return;
    }
    this.requestToSession.delete(requestId);
    this.respondedRequestToSession.delete(requestId);
    this.emit("permissionResolved", {
      requestId,
      outcome
    });
  }
  handleModelSwitched(sessionId, data) {
    if (!isRecord(data) || typeof data["modelId"] !== "string") {
      this.emitProtocolError("Malformed daemon model_switched event", data);
      return;
    }
    this.emit("modelSwitched", {
      sessionId,
      modelId: data["modelId"]
    });
  }
  handleModelSwitchFailed(sessionId, data) {
    if (!isRecord(data)) {
      this.emitProtocolError("Malformed daemon model_switch_failed event", data);
      return;
    }
    this.emit("modelSwitchFailed", {
      sessionId,
      requestedModelId: getString(data["requestedModelId"]),
      error: getString(data["error"]) ?? "model_switch_failed"
    });
  }
  handleSessionDied(sessionId, data) {
    this.dropSession(sessionId, this.getStringField(data, "reason", "session_died"));
  }
  dropSession(sessionId, reason) {
    if (!this.sessions.has(sessionId)) {
      return;
    }
    this.eventControllers.get(sessionId)?.abort();
    this.eventControllers.delete(sessionId);
    this.sessions.delete(sessionId);
    this.abortActivePrompts(sessionId);
    this.activePrompts.delete(sessionId);
    this.availableCommandsBySession.delete(sessionId);
    if (this.latestAvailableCommandsSessionId === sessionId) {
      this.latestAvailableCommandsSessionId = Array.from(this.availableCommandsBySession.keys()).at(-1);
    }
    for (const [requestId, mappedSessionId] of this.requestToSession) {
      if (mappedSessionId === sessionId) {
        this.requestToSession.delete(requestId);
      }
    }
    for (const [requestId, mappedSessionId] of this.respondedRequestToSession) {
      if (mappedSessionId === sessionId) {
        this.respondedRequestToSession.delete(requestId);
      }
    }
    this.emit("sessionDied", { sessionId, reason });
  }
  getStringField(data, field, fallback) {
    return isRecord(data) && typeof data[field] === "string" ? data[field] : fallback;
  }
  abortActivePrompts(sessionId) {
    const promptControllers = this.activePromptControllers.get(sessionId);
    if (!promptControllers) {
      return;
    }
    for (const controller of promptControllers) {
      controller.abort();
    }
    this.activePromptControllers.delete(sessionId);
  }
  emitProtocolError(message, details) {
    const error = new Error(message);
    error.details = summarizeProtocolDetails(details);
    this.emit("error", error);
  }
};

// packages/channels/base/dist/BlockStreamer.js
init_esbuild_shims();
var BlockStreamer = class {
  static {
    __name(this, "BlockStreamer");
  }
  buffer = "";
  idleTimer = null;
  sending = Promise.resolve();
  opts;
  /** Number of blocks emitted so far. */
  blockCount = 0;
  constructor(opts) {
    this.opts = opts;
  }
  /** Feed a new text chunk from the agent stream. */
  push(chunk) {
    this.buffer += chunk;
    this.clearIdleTimer();
    this.checkEmit();
    if (this.buffer.length > 0 && this.opts.idleMs > 0) {
      this.idleTimer = setTimeout(() => this.onIdle(), this.opts.idleMs);
    }
  }
  /** Flush all remaining buffered text. Awaits all pending sends. */
  async flush() {
    this.clearIdleTimer();
    if (this.buffer.length > 0) {
      this.emitBlock(this.buffer);
      this.buffer = "";
    }
    await this.sending;
  }
  /** Drop buffered text and cancel future idle emission. */
  stop() {
    this.clearIdleTimer();
    this.buffer = "";
  }
  // ---------------------------------------------------------------------------
  // Internal
  // ---------------------------------------------------------------------------
  checkEmit() {
    while (this.buffer.length >= this.opts.maxChars) {
      const bp = this.findBreakPoint(this.buffer, this.opts.maxChars);
      this.emitBlock(this.buffer.slice(0, bp));
      this.buffer = this.buffer.slice(bp);
    }
    if (this.buffer.length >= this.opts.minChars) {
      const bp = this.findBlockBoundary(this.buffer);
      if (bp > 0) {
        this.emitBlock(this.buffer.slice(0, bp));
        this.buffer = this.buffer.slice(bp);
      }
    }
  }
  onIdle() {
    this.idleTimer = null;
    if (this.buffer.length >= this.opts.minChars) {
      this.emitBlock(this.buffer);
      this.buffer = "";
    }
  }
  emitBlock(text) {
    const trimmed = text.trim();
    if (!trimmed)
      return;
    this.blockCount++;
    this.sending = this.sending.then(() => this.opts.send(trimmed)).catch(() => {
    });
  }
  /**
   * Find the last paragraph boundary (\n\n) in the buffer.
   * Returns the position after the boundary, or -1 if no suitable boundary
   * exists at or after minChars.
   */
  findBlockBoundary(text) {
    const last = text.lastIndexOf("\n\n");
    if (last < 0 || last < this.opts.minChars)
      return -1;
    return last + 2;
  }
  /**
   * Find the best break point at or before maxPos.
   * Prefers paragraph break > newline > space > maxPos.
   */
  findBreakPoint(text, maxPos) {
    const sub = text.slice(0, maxPos);
    const para = sub.lastIndexOf("\n\n");
    if (para > 0)
      return para + 2;
    const nl = sub.lastIndexOf("\n");
    if (nl > 0)
      return nl + 1;
    const sp = sub.lastIndexOf(" ");
    if (sp > 0)
      return sp + 1;
    return maxPos;
  }
  clearIdleTimer() {
    if (this.idleTimer !== null) {
      clearTimeout(this.idleTimer);
      this.idleTimer = null;
    }
  }
};

// packages/channels/base/dist/ChannelBase.js
init_esbuild_shims();
import { basename, join as join4 } from "node:path";

// packages/channels/base/dist/GroupGate.js
init_esbuild_shims();
var GroupGate = class {
  static {
    __name(this, "GroupGate");
  }
  policy;
  groups;
  constructor(policy = "disabled", groups = {}) {
    this.policy = policy;
    this.groups = groups;
  }
  /**
   * Full group check: policy + allowlist + mention gating.
   * Evaluation order:
   *   1. groupPolicy (disabled → drop)
   *   2. group allowlist (allowlist mode, no match → drop)
   *   3. mention gating (requireMention + not mentioned → drop silently)
   *
   * Mention gating runs before sender gate so that unmentioned messages
   * in groups don't trigger pairing flows.
   */
  check(envelope) {
    if (!envelope.isGroup) {
      return { allowed: true };
    }
    if (this.policy === "disabled") {
      return { allowed: false, reason: "disabled" };
    }
    if (this.policy === "allowlist") {
      if (!this.groups[envelope.chatId]) {
        return { allowed: false, reason: "not_allowlisted" };
      }
    }
    const groupConfig = this.groups[envelope.chatId] || this.groups["*"] || {};
    const requireMention = groupConfig.requireMention ?? true;
    if (requireMention && !envelope.isMentioned && !envelope.isReplyToBot) {
      return { allowed: false, reason: "mention_required" };
    }
    return { allowed: true };
  }
};

// packages/channels/base/dist/group-history-store.js
init_esbuild_shims();
import { appendFileSync, chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
import { dirname, join as join2 } from "node:path";
var DEFAULT_MAX_KEYS = 1e3;
var DEFAULT_COMPACT_AFTER_RECORDS = 1e3;
var GroupHistoryStore = class {
  static {
    __name(this, "GroupHistoryStore");
  }
  filePath;
  maxKeys;
  compactAfterRecords;
  constructor(filePath, options = {}) {
    this.filePath = filePath;
    this.maxKeys = options.maxKeys ?? DEFAULT_MAX_KEYS;
    this.compactAfterRecords = options.compactAfterRecords ?? DEFAULT_COMPACT_AFTER_RECORDS;
  }
  record(key, entry, limit) {
    const normalizedLimit = normalizeLimit(limit);
    if (normalizedLimit <= 0) {
      return;
    }
    const loaded = this.loadState();
    const state = loaded.entries;
    const limits = loaded.limits;
    const current = state.get(key) ?? [];
    current.push(entry);
    if (current.length > normalizedLimit) {
      current.splice(0, current.length - normalizedLimit);
    }
    state.delete(key);
    state.set(key, current);
    limits.set(key, normalizedLimit);
    const evicted = evictOldKeys(state, this.maxKeys, limits);
    this.append({
      type: "message",
      key,
      limit: normalizedLimit,
      entry,
      recordedAt: Date.now()
    });
    if (evicted || loaded.hadInvalidRecords || loaded.recordCount + 1 >= this.compactAfterRecords) {
      this.compact(state, limits);
    }
  }
  drain(key, limit) {
    const normalizedLimit = normalizeLimit(limit);
    const loaded = this.loadState();
    const state = loaded.entries;
    const entries = normalizedLimit > 0 ? (state.get(key) ?? []).slice(-normalizedLimit) : [];
    if (state.has(key)) {
      state.delete(key);
      loaded.limits.delete(key);
      this.append({ type: "clear", key, recordedAt: Date.now() });
    }
    return entries;
  }
  clear(key) {
    const loaded = this.loadState();
    const state = loaded.entries;
    if (!state.has(key)) {
      return;
    }
    state.delete(key);
    loaded.limits.delete(key);
    this.append({ type: "clear", key, recordedAt: Date.now() });
    this.compact(state, loaded.limits);
  }
  clearAll() {
    const loaded = this.loadState();
    if (loaded.entries.size === 0) {
      return;
    }
    const recordedAt = Date.now();
    for (const key of loaded.entries.keys()) {
      this.append({ type: "clear", key, recordedAt });
    }
    this.compact(/* @__PURE__ */ new Map(), /* @__PURE__ */ new Map());
  }
  size(key) {
    const state = this.loadState().entries;
    if (key !== void 0) {
      return state.get(key)?.length ?? 0;
    }
    return state.size;
  }
  loadState() {
    const state = /* @__PURE__ */ new Map();
    const limits = /* @__PURE__ */ new Map();
    const read = this.readRecords();
    for (const record of read.records) {
      if (record.type === "clear") {
        state.delete(record.key);
        limits.delete(record.key);
        continue;
      }
      const current = state.get(record.key) ?? [];
      current.push(record.entry);
      if (current.length > record.limit) {
        current.splice(0, current.length - record.limit);
      }
      state.delete(record.key);
      state.set(record.key, current);
      limits.set(record.key, record.limit);
      evictOldKeys(state, this.maxKeys, limits);
    }
    return {
      entries: state,
      limits,
      recordCount: read.records.length,
      hadInvalidRecords: read.hadInvalidRecords
    };
  }
  readRecords() {
    if (!existsSync(this.filePath)) {
      return { records: [], hadInvalidRecords: false };
    }
    let data;
    try {
      data = readFileSync(this.filePath, "utf-8");
    } catch (err) {
      if (isErrnoCode(err, "ENOENT")) {
        return { records: [], hadInvalidRecords: false };
      }
      throw err;
    }
    const records = [];
    let hadInvalidRecords = false;
    for (const line of data.split("\n")) {
      if (line.trim().length === 0) {
        continue;
      }
      try {
        const parsed = JSON.parse(line);
        if (isGroupHistoryRecord(parsed)) {
          records.push(parsed);
        } else {
          hadInvalidRecords = true;
        }
      } catch {
        hadInvalidRecords = true;
      }
    }
    return { records, hadInvalidRecords };
  }
  append(record) {
    const dir = dirname(this.filePath);
    mkdirSync(dir, { recursive: true, mode: 448 });
    chmodPrivate(dir, 448);
    appendFileSync(this.filePath, `${JSON.stringify(record)}
`, {
      encoding: "utf-8",
      mode: 384
    });
    chmodPrivate(this.filePath, 384);
  }
  compact(state, limits) {
    const dir = dirname(this.filePath);
    mkdirSync(dir, { recursive: true, mode: 448 });
    chmodPrivate(dir, 448);
    const records = [];
    const recordedAt = Date.now();
    for (const [key, entries] of state) {
      for (const entry of entries) {
        records.push({
          type: "message",
          key,
          limit: limits.get(key) ?? entries.length,
          entry,
          recordedAt
        });
      }
    }
    const data = records.length > 0 ? `${records.map((record) => JSON.stringify(record)).join("\n")}
` : "";
    const tempPath = join2(dir, `${Date.now()}-${process.pid}-${Math.random().toString(16).slice(2)}.tmp`);
    writeFileSync(tempPath, data, { encoding: "utf-8", mode: 384 });
    chmodPrivate(tempPath, 384);
    renameSync(tempPath, this.filePath);
    chmodPrivate(this.filePath, 384);
  }
};
function normalizeLimit(limit) {
  if (!Number.isFinite(limit) || limit <= 0) {
    return 0;
  }
  return Math.floor(limit);
}
__name(normalizeLimit, "normalizeLimit");
function evictOldKeys(state, maxKeys, limits) {
  let evicted = false;
  while (state.size > maxKeys) {
    const oldest = state.keys().next().value;
    if (oldest === void 0) {
      return evicted;
    }
    state.delete(oldest);
    limits?.delete(oldest);
    evicted = true;
  }
  return evicted;
}
__name(evictOldKeys, "evictOldKeys");
function isGroupHistoryRecord(value) {
  if (!isRecord2(value)) {
    return false;
  }
  if (value["type"] === "clear") {
    return typeof value["key"] === "string";
  }
  if (value["type"] !== "message") {
    return false;
  }
  return typeof value["key"] === "string" && typeof value["limit"] === "number" && isGroupHistoryEntry(value["entry"]);
}
__name(isGroupHistoryRecord, "isGroupHistoryRecord");
function isGroupHistoryEntry(value) {
  return isRecord2(value) && typeof value["senderId"] === "string" && typeof value["senderName"] === "string" && typeof value["text"] === "string" && typeof value["timestamp"] === "number";
}
__name(isGroupHistoryEntry, "isGroupHistoryEntry");
function isRecord2(value) {
  return typeof value === "object" && value !== null;
}
__name(isRecord2, "isRecord");
function isErrnoCode(err, code) {
  return err instanceof Error && "code" in err && err.code === code;
}
__name(isErrnoCode, "isErrnoCode");
function chmodPrivate(path4, mode) {
  try {
    chmodSync(path4, mode);
  } catch {
  }
}
__name(chmodPrivate, "chmodPrivate");

// packages/channels/base/dist/SenderGate.js
init_esbuild_shims();
var SenderGate = class {
  static {
    __name(this, "SenderGate");
  }
  policy;
  allowedUsers;
  pairingStore;
  constructor(policy, allowedUsers = [], pairingStore) {
    this.policy = policy;
    this.allowedUsers = new Set(allowedUsers);
    this.pairingStore = pairingStore || null;
  }
  isAllowed(senderId) {
    switch (this.policy) {
      case "open":
        return true;
      case "allowlist":
        return this.allowedUsers.has(senderId);
      case "pairing":
        return this.allowedUsers.has(senderId) || this.pairingStore?.isApproved(senderId) === true;
      default:
        throw new Error(`Unknown sender policy: ${this.policy}`);
    }
  }
  check(senderId, senderName) {
    switch (this.policy) {
      case "open":
        return { allowed: true };
      case "allowlist":
        return { allowed: this.allowedUsers.has(senderId) };
      case "pairing": {
        if (this.allowedUsers.has(senderId)) {
          return { allowed: true };
        }
        if (this.pairingStore?.isApproved(senderId)) {
          return { allowed: true };
        }
        const code = this.pairingStore?.createRequest(senderId, senderName || senderId);
        return { allowed: false, pairingCode: code ?? null };
      }
      default:
        throw new Error(`Unknown sender policy: ${this.policy}`);
    }
  }
};

// packages/channels/base/dist/PairingStore.js
init_esbuild_shims();
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as path2 from "node:path";
var SAFE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
var CODE_LENGTH = 8;
var EXPIRY_MS = 60 * 60 * 1e3;
var MAX_PENDING = 3;
var PairingStore = class {
  static {
    __name(this, "PairingStore");
  }
  dir;
  pendingPath;
  allowlistPath;
  constructor(channelName) {
    this.dir = path2.join(getGlobalQwenDir(), "channels");
    this.pendingPath = path2.join(this.dir, `${channelName}-pairing.json`);
    this.allowlistPath = path2.join(this.dir, `${channelName}-allowlist.json`);
  }
  isApproved(senderId) {
    const list = this.readAllowlist();
    return list.includes(senderId);
  }
  /**
   * Create a pairing request for an unknown sender.
   * Returns the code if created, or null if the pending cap is reached.
   * If the sender already has a non-expired pending request, returns that code.
   */
  createRequest(senderId, senderName) {
    const pending = this.readPending();
    const now = Date.now();
    const active = pending.filter((r) => now - r.createdAt < EXPIRY_MS);
    const existing = active.find((r) => r.senderId === senderId);
    if (existing) {
      return existing.code;
    }
    if (active.length >= MAX_PENDING) {
      return null;
    }
    const code = generateCode();
    active.push({ senderId, senderName, code, createdAt: now });
    this.writePending(active);
    return code;
  }
  /**
   * Approve a pairing request by code.
   * Returns the sender ID if found, or null if not found / expired.
   */
  approve(code) {
    const pending = this.readPending();
    const now = Date.now();
    const idx = pending.findIndex((r) => r.code === code.toUpperCase() && now - r.createdAt < EXPIRY_MS);
    if (idx === -1)
      return null;
    const request = pending[idx];
    pending.splice(idx, 1);
    this.writePending(pending);
    const list = this.readAllowlist();
    if (!list.includes(request.senderId)) {
      list.push(request.senderId);
      this.writeAllowlist(list);
    }
    return request;
  }
  listPending() {
    const pending = this.readPending();
    const now = Date.now();
    return pending.filter((r) => now - r.createdAt < EXPIRY_MS);
  }
  getAllowlist() {
    return this.readAllowlist();
  }
  ensureDir() {
    if (!fs.existsSync(this.dir)) {
      fs.mkdirSync(this.dir, { recursive: true });
    }
  }
  readPending() {
    try {
      const data = fs.readFileSync(this.pendingPath, "utf-8");
      return JSON.parse(data);
    } catch {
      return [];
    }
  }
  writePending(requests) {
    this.ensureDir();
    fs.writeFileSync(this.pendingPath, JSON.stringify(requests, null, 2));
  }
  readAllowlist() {
    try {
      const data = fs.readFileSync(this.allowlistPath, "utf-8");
      return JSON.parse(data);
    } catch {
      return [];
    }
  }
  writeAllowlist(list) {
    this.ensureDir();
    fs.writeFileSync(this.allowlistPath, JSON.stringify(list, null, 2));
  }
};
function generateCode() {
  let code = "";
  for (let i = 0; i < CODE_LENGTH; i++) {
    code += SAFE_ALPHABET[crypto.randomInt(SAFE_ALPHABET.length)];
  }
  return code;
}
__name(generateCode, "generateCode");

// packages/channels/base/dist/SessionRouter.js
init_esbuild_shims();
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync } from "node:fs";
import process2 from "node:process";
var SessionRouter = class {
  static {
    __name(this, "SessionRouter");
  }
  toSession = /* @__PURE__ */ new Map();
  // routing key → session ID
  toTarget = /* @__PURE__ */ new Map();
  // session ID → target
  toCwd = /* @__PURE__ */ new Map();
  // session ID → cwd
  creatingSessions = /* @__PURE__ */ new Map();
  sessionLoadWindows = /* @__PURE__ */ new Set();
  bridge;
  defaultCwd;
  defaultScope;
  channelScopes = /* @__PURE__ */ new Map();
  persistPath;
  constructor(bridge, defaultCwd, scope = "user", persistPath) {
    this.bridge = bridge;
    this.defaultCwd = defaultCwd;
    this.defaultScope = scope;
    this.persistPath = persistPath;
  }
  /** Replace the bridge instance (used after crash recovery restart). */
  setBridge(bridge) {
    this.bridge = bridge;
  }
  /** Set scope override for a specific channel. */
  setChannelScope(channelName, scope) {
    this.channelScopes.set(channelName, scope);
  }
  routingKey(channelName, senderId, chatId, threadId) {
    const scope = this.channelScopes.get(channelName) || this.defaultScope;
    switch (scope) {
      case "thread":
        return `${channelName}:${threadId || chatId}`;
      case "single":
        return `${channelName}:__single__`;
      case "user":
      default:
        return `${channelName}:${senderId}:${chatId}`;
    }
  }
  async resolve(channelName, senderId, chatId, threadId, cwd) {
    const key = this.routingKey(channelName, senderId, chatId, threadId);
    let failedCreateWaits = 0;
    for (; ; ) {
      const existing = this.toSession.get(key);
      if (existing) {
        return existing;
      }
      const creating = this.creatingSessions.get(key);
      if (creating) {
        try {
          return await creating;
        } catch (err) {
          if (this.creatingSessions.get(key) === creating) {
            this.creatingSessions.delete(key);
          }
          failedCreateWaits++;
          if (failedCreateWaits > 3) {
            throw err;
          }
          continue;
        }
      }
      const created = Promise.resolve().then(async () => {
        const sessionCwd = cwd || this.defaultCwd;
        const loadWindow = this.beginSessionLoad();
        try {
          const sessionId = await this.createLiveSession(sessionCwd, loadWindow, key);
          this.toSession.set(key, sessionId);
          this.toTarget.set(sessionId, {
            channelName,
            senderId,
            chatId,
            threadId
          });
          this.toCwd.set(sessionId, sessionCwd);
          this.persist();
          return sessionId;
        } finally {
          this.endSessionLoad(loadWindow);
        }
      });
      this.creatingSessions.set(key, created);
      try {
        return await created;
      } finally {
        if (this.creatingSessions.get(key) === created) {
          this.creatingSessions.delete(key);
        }
      }
    }
  }
  getTarget(sessionId) {
    return this.toTarget.get(sessionId);
  }
  getSession(channelName, senderId, chatId, threadId) {
    return this.toSession.get(this.routingKey(channelName, senderId, chatId, threadId));
  }
  hasSession(channelName, senderId, chatId, threadId) {
    const scope = this.channelScopes.get(channelName) || this.defaultScope;
    if (chatId) {
      return this.toSession.has(this.routingKey(channelName, senderId, chatId, threadId));
    }
    if (scope === "single") {
      return false;
    }
    for (const target of this.toTarget.values()) {
      if (target.channelName === channelName && target.senderId === senderId) {
        return true;
      }
    }
    return false;
  }
  /**
   * Remove session(s) for the given sender. Returns the removed session IDs.
   */
  removeSession(channelName, senderId, chatId, threadId) {
    const removedIds = [];
    const scope = this.channelScopes.get(channelName) || this.defaultScope;
    if (chatId) {
      const key = this.routingKey(channelName, senderId, chatId, threadId);
      const sessionId = this.deleteByKey(key);
      if (sessionId)
        removedIds.push(sessionId);
    } else if (scope === "single") {
      return removedIds;
    } else {
      for (const [k, mappedSessionId] of [...this.toSession.entries()]) {
        const target = this.toTarget.get(mappedSessionId);
        if (target?.channelName === channelName && target.senderId === senderId) {
          const sessionId = this.deleteByKey(k);
          if (sessionId)
            removedIds.push(sessionId);
        }
      }
    }
    if (removedIds.length > 0)
      this.persist();
    return removedIds;
  }
  /** Remove a session mapping by daemon/ACP session ID. */
  removeSessionId(sessionId) {
    let removed = false;
    for (const [key, mappedSessionId] of [...this.toSession.entries()]) {
      if (mappedSessionId === sessionId) {
        this.toSession.delete(key);
        removed = true;
      }
    }
    if (this.toTarget.delete(sessionId)) {
      removed = true;
    }
    if (this.toCwd.delete(sessionId)) {
      removed = true;
    }
    if (!removed && this.sessionLoadWindows.size > 0) {
      for (const loadWindow of this.sessionLoadWindows) {
        loadWindow.add(sessionId);
      }
    }
    if (removed) {
      this.persist();
    }
    return removed;
  }
  deleteByKey(key) {
    const sessionId = this.toSession.get(key);
    if (!sessionId)
      return null;
    this.toSession.delete(key);
    this.toTarget.delete(sessionId);
    this.toCwd.delete(sessionId);
    return sessionId;
  }
  /** Get all session entries for crash recovery. */
  getAll() {
    const entries = [];
    for (const [key, sessionId] of this.toSession) {
      const target = this.toTarget.get(sessionId);
      if (target) {
        entries.push({ key, sessionId, target });
      }
    }
    return entries;
  }
  /**
   * Restore session mappings from a previous bridge.
   * Called after bridge restart — attempts loadSession for each saved mapping.
   * Failed loads are dropped (new session on next message).
   */
  async restoreSessions() {
    const persistPath = this.persistPath;
    if (!persistPath || !existsSync3(persistPath)) {
      return { restored: 0, failed: 0 };
    }
    let entries;
    try {
      entries = JSON.parse(readFileSync3(persistPath, "utf-8"));
    } catch (err) {
      const reason = err instanceof Error ? err.message : String(err);
      process2.stderr.write(`[SessionRouter] Corrupted persist file at ${sanitizeLogText(persistPath, 1024)}: ${sanitizeLogText(reason, 512)}
`);
      return { restored: 0, failed: 0 };
    }
    let restored = 0;
    let failed = 0;
    let changed = false;
    const reservations = /* @__PURE__ */ new Map();
    for (const key of Object.keys(entries)) {
      this.deleteByKey(key);
      const reservation = this.createSessionReservation();
      reservation.promise.catch(() => void 0);
      this.creatingSessions.set(key, reservation.promise);
      reservations.set(key, reservation);
    }
    const loadWindow = this.beginSessionLoad();
    try {
      for (const [key, entry] of Object.entries(entries)) {
        const reservation = reservations.get(key);
        if (!reservation)
          continue;
        try {
          const sessionId = await this.bridge.loadSession(entry.sessionId, entry.cwd);
          if (typeof sessionId !== "string" || sessionId.length === 0) {
            throw new Error("Invalid restored session ID");
          }
          if (loadWindow.delete(sessionId)) {
            throw new Error("Restored session died before routing completed");
          }
          this.toSession.set(key, sessionId);
          this.toTarget.set(sessionId, entry.target);
          this.toCwd.set(sessionId, entry.cwd);
          reservation.resolve(sessionId);
          if (sessionId !== entry.sessionId) {
            changed = true;
          }
          restored++;
        } catch (err) {
          const reason = err instanceof Error ? err.message : String(err);
          process2.stderr.write(`[SessionRouter] Failed to restore session ${sanitizeLogText(entry.sessionId, 128)} for key ${sanitizeLogText(key, 256)}: ${sanitizeLogText(reason, 512)}
`);
          reservation.reject(new Error("Session restore failed", { cause: err }));
          failed++;
          changed = true;
        } finally {
          if (this.creatingSessions.get(key) === reservation.promise) {
            this.creatingSessions.delete(key);
          }
        }
      }
    } finally {
      this.endSessionLoad(loadWindow);
    }
    if (changed) {
      this.persist();
    }
    return { restored, failed };
  }
  /** Clear in-memory state and delete persist file. Used on clean shutdown. */
  clearAll() {
    this.toSession.clear();
    this.toTarget.clear();
    this.toCwd.clear();
    this.creatingSessions.clear();
    this.sessionLoadWindows.clear();
    if (this.persistPath && existsSync3(this.persistPath)) {
      try {
        unlinkSync(this.persistPath);
      } catch {
      }
    }
  }
  persist() {
    if (!this.persistPath)
      return;
    const data = {};
    for (const [key, sessionId] of this.toSession) {
      const target = this.toTarget.get(sessionId);
      if (target) {
        data[key] = {
          sessionId,
          target,
          cwd: this.toCwd.get(sessionId) || this.defaultCwd
        };
      }
    }
    try {
      writeFileSync3(this.persistPath, JSON.stringify(data, null, 2), "utf-8");
    } catch {
    }
  }
  async createLiveSession(cwd, loadWindow, routingKey) {
    const maxAttempts = 2;
    let lastDeadSessionId;
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      const sessionId = await this.bridge.newSession(cwd);
      if (typeof sessionId !== "string" || sessionId.length === 0) {
        throw new Error("Invalid session ID from bridge");
      }
      if (!loadWindow.delete(sessionId)) {
        return sessionId;
      }
      lastDeadSessionId = sessionId;
    }
    throw new Error(`Session ${lastDeadSessionId ?? "unknown"} died before routing completed (${maxAttempts}/${maxAttempts} attempts, key ${routingKey})`);
  }
  beginSessionLoad() {
    const loadWindow = /* @__PURE__ */ new Set();
    this.sessionLoadWindows.add(loadWindow);
    return loadWindow;
  }
  createSessionReservation() {
    let resolveReservation;
    let rejectReservation;
    const promise = new Promise((resolve2, reject) => {
      resolveReservation = resolve2;
      rejectReservation = reject;
    });
    return {
      promise,
      resolve: resolveReservation,
      reject: rejectReservation
    };
  }
  endSessionLoad(loadWindow) {
    this.sessionLoadWindows.delete(loadWindow);
  }
};

// packages/channels/base/dist/ChannelLoopScheduler.js
init_esbuild_shims();
var MAX_RESULT_PREVIEW_LENGTH = 500;
var MAX_CONCURRENT_LOOP_FIRES = 5;
var ChannelLoopSkippedError = class extends Error {
  static {
    __name(this, "ChannelLoopSkippedError");
  }
};
var ChannelLoopScheduler = class {
  static {
    __name(this, "ChannelLoopScheduler");
  }
  store;
  channels;
  nextFireTime;
  now;
  maxConsecutiveFailures;
  intervalMs;
  loopTimeoutMs;
  timer;
  runningTick;
  inFlightJobs = /* @__PURE__ */ new Map();
  generation = 0;
  constructor(options) {
    this.store = options.store;
    this.channels = options.channels;
    this.nextFireTime = options.nextFireTime;
    this.now = options.now ?? (() => /* @__PURE__ */ new Date());
    this.maxConsecutiveFailures = options.maxConsecutiveFailures ?? 5;
    this.intervalMs = options.intervalMs ?? 6e4;
    this.loopTimeoutMs = options.loopTimeoutMs ?? 5 * 6e4;
  }
  start() {
    if (this.timer)
      return;
    const generation = this.generation;
    const startup = this.reconcileStartupState();
    void startup.then(() => {
      if (!this.timer || this.generation !== generation)
        return;
      return this.tick();
    }).catch((err) => {
      process.stderr.write(`[scheduler] initial tick failed: ${err}
`);
    });
    this.timer = setInterval(() => {
      void this.tick().catch((err) => {
        process.stderr.write(`[scheduler] interval tick failed: ${err}
`);
      });
    }, this.intervalMs);
    this.timer.unref?.();
  }
  stop() {
    if (this.timer) {
      clearInterval(this.timer);
    }
    this.timer = void 0;
    this.runningTick = void 0;
    this.generation++;
    this.inFlightJobs.clear();
  }
  async reconcileStartupState() {
    const jobs = await this.store.list();
    const staleRunning = jobs.filter((job) => job.runningSince);
    for (const job of staleRunning) {
      await this.clearRunningSince(job.id);
    }
    const enabledCount = jobs.filter((job) => job.enabled).length;
    process.stderr.write(`[scheduler] started, tick interval ${this.intervalMs}ms, jobs ${jobs.length}, enabled ${enabledCount}, cleared stale running ${staleRunning.length}
`);
  }
  async tick() {
    if (this.runningTick)
      return this.runningTick;
    const tick = this.runTick().finally(() => {
      if (this.runningTick === tick) {
        this.runningTick = void 0;
      }
    });
    this.runningTick = tick;
    return this.runningTick;
  }
  async runTick() {
    const generation = this.generation;
    const now = this.now();
    const jobs = await this.store.list();
    if (this.generation !== generation) {
      return;
    }
    const dueJobs = jobs.filter((job) => job.enabled && this.channels.has(job.channelName) && !this.inFlightJobs.has(job.id) && this.isDue(job, now));
    const availableSlots = MAX_CONCURRENT_LOOP_FIRES - this.inFlightJobs.size;
    if (availableSlots <= 0)
      return;
    for (const job of dueJobs.slice(0, availableSlots)) {
      void this.fireOnce(job, now, generation);
    }
  }
  isDue(job, now) {
    try {
      const after = new Date(lastAnchor(job));
      return this.nextFireTime(job.cron, after).getTime() <= now.getTime();
    } catch (err) {
      process.stderr.write(`[scheduler] invalid cron for loop ${job.id}: ${err}
`);
      return false;
    }
  }
  async fireOnce(job, now, generation) {
    const token = Symbol(job.id);
    this.inFlightJobs.set(job.id, token);
    try {
      await this.fire(job, now, generation);
    } catch (err) {
      process.stderr.write(`[scheduler] unhandled error for loop ${job.id}: ${err instanceof Error ? err.message : String(err)}
`);
    } finally {
      if (this.inFlightJobs.get(job.id) === token) {
        this.inFlightJobs.delete(job.id);
      }
    }
  }
  async fire(job, now, generation) {
    const channel = this.channels.get(job.channelName);
    if (!channel) {
      return;
    }
    const latestJob = await this.findJob(job.id);
    if (!latestJob?.enabled)
      return;
    if (this.generation !== generation)
      return;
    const runningSince = now.toISOString();
    let resultPreview;
    try {
      await this.store.update(latestJob.id, {
        runningSince,
        lastFiredAt: runningSince
      });
      if (this.generation !== generation) {
        await this.clearRunningSince(latestJob.id, runningSince);
        return;
      }
      resultPreview = await channel.runLoopPrompt(latestJob, {
        timeoutMs: this.loopTimeoutMs,
        shouldContinue: /* @__PURE__ */ __name(async () => {
          if (this.generation !== generation) {
            return false;
          }
          const currentJob2 = await this.findJob(latestJob.id);
          return currentJob2?.enabled === true;
        }, "shouldContinue")
      });
    } catch (err) {
      if (err instanceof ChannelLoopSkippedError) {
        await this.recordSkipped(latestJob.id, runningSince);
        return;
      }
      let currentJob2;
      try {
        currentJob2 = await this.findJob(latestJob.id);
      } catch (findErr) {
        process.stderr.write(`[scheduler] findJob failed in catch for loop ${latestJob.id}: ${findErr instanceof Error ? findErr.message : String(findErr)}
`);
        await this.clearRunningSince(latestJob.id, runningSince);
        return;
      }
      if (this.generation !== generation || !currentJob2?.enabled) {
        await this.clearRunningSince(latestJob.id, runningSince);
        return;
      }
      await this.recordFailure(currentJob2, now, err instanceof Error ? err.message : String(err));
      return;
    }
    const currentJob = await this.findJob(latestJob.id);
    if (!currentJob || currentJob.runningSince !== runningSince) {
      return;
    }
    const finishedAt = this.now();
    const patch = {
      lastFiredAt: runningSince,
      lastFinishedAt: finishedAt.toISOString(),
      lastResultPreview: truncateResultPreview(resultPreview),
      lastStatus: "ok",
      lastError: void 0,
      consecutiveFailures: 0,
      runningSince: void 0,
      runCount: currentJob.runCount + 1
    };
    if (!currentJob.recurring) {
      patch.enabled = false;
    }
    try {
      await this.store.update(latestJob.id, patch);
    } catch (err) {
      process.stderr.write(`[scheduler] loop ${latestJob.id} succeeded but status persist failed: ${err instanceof Error ? err.message : String(err)}
`);
      await this.clearRunningSince(latestJob.id, runningSince);
    }
  }
  async findJob(id) {
    const jobs = await this.store.list();
    return jobs.find((job) => job.id === id);
  }
  async clearRunningSince(id, expectedRunningSince) {
    try {
      if (expectedRunningSince) {
        const currentJob = await this.findJob(id);
        if (currentJob?.runningSince !== expectedRunningSince)
          return;
      }
      await this.store.update(id, { runningSince: void 0 });
    } catch (err) {
      process.stderr.write(`[scheduler] failed to clear running state for loop ${id}: ${err instanceof Error ? err.message : String(err)}
`);
    }
  }
  async recordSkipped(id, expectedRunningSince) {
    try {
      const currentJob = await this.findJob(id);
      if (!currentJob || currentJob.runningSince !== expectedRunningSince) {
        return;
      }
      await this.store.update(id, {
        lastFinishedAt: this.now().toISOString(),
        runningSince: void 0
      });
    } catch (err) {
      process.stderr.write(`[scheduler] failed to record skipped loop ${id}: ${err instanceof Error ? err.message : String(err)}
`);
    }
  }
  async recordFailure(job, now, message) {
    const consecutiveFailures = job.consecutiveFailures + 1;
    const patch = {
      lastFiredAt: now.toISOString(),
      lastFinishedAt: this.now().toISOString(),
      lastStatus: "error",
      lastError: truncateError(message),
      lastResultPreview: void 0,
      consecutiveFailures,
      runningSince: void 0,
      runCount: job.runCount + 1
    };
    if (!job.recurring) {
      patch.enabled = false;
    }
    if (consecutiveFailures >= this.maxConsecutiveFailures) {
      patch.enabled = false;
      process.stderr.write(`[scheduler] loop ${job.id} auto-disabled after ${consecutiveFailures} consecutive failures
`);
    }
    try {
      await this.store.update(job.id, patch);
    } catch (err) {
      process.stderr.write(`[scheduler] loop ${job.id} failure persist failed: ${err instanceof Error ? err.message : String(err)}
`);
      await this.clearRunningSince(job.id);
    }
  }
};
function lastAnchor(job) {
  if (job.lastFiredAt && job.lastFinishedAt) {
    return new Date(job.lastFiredAt).getTime() > new Date(job.lastFinishedAt).getTime() ? job.lastFiredAt : job.lastFinishedAt;
  }
  return job.lastFinishedAt ?? job.lastFiredAt ?? job.createdAt;
}
__name(lastAnchor, "lastAnchor");
function truncateResultPreview(text) {
  return text === void 0 ? void 0 : text.slice(0, MAX_RESULT_PREVIEW_LENGTH);
}
__name(truncateResultPreview, "truncateResultPreview");
function truncateError(message) {
  return message.slice(0, 1e3);
}
__name(truncateError, "truncateError");

// packages/channels/base/dist/ChannelBase.js
var CLEAR_CANCEL_TIMEOUT_MS = 3e3;
var GROUP_HISTORY_CONTEXT_MARKER = "[Chat messages since your last reply - for context]";
var CURRENT_MESSAGE_MARKER = "[Current message - respond to this]";
var GROUP_HISTORY_ENTRY_TEXT_LIMIT = 1e3;
var GROUP_HISTORY_ENTRY_METADATA_LIMIT = 256;
var LOOP_CANCEL_GRACE_MS = 5e3;
var COMMAND_TOKEN_CHARS = "a-zA-Z0-9_:-";
var PARSE_COMMAND_RE = new RegExp(`^\\/([${COMMAND_TOKEN_CHARS}]+)(?:@\\S+)?\\s*(.*)`, "s");
var COMMAND_TOKEN_RE = new RegExp(`^[${COMMAND_TOKEN_CHARS}]+(?:@\\S+)?$`);
var LOOP_ADD_RE = /^"([^"]+)"\s+(.+)$/su;
var MAX_LOOP_JOBS_PER_TARGET = 10;
var MAX_LOOP_PROMPT_CHARS = 4e3;
function parseLoopAddArgs(args) {
  const match = args.trim().match(LOOP_ADD_RE);
  if (!match)
    return null;
  const cron = match[1].trim();
  const prompt = match[2].trim();
  return cron && prompt ? { cron, prompt } : null;
}
__name(parseLoopAddArgs, "parseLoopAddArgs");
var ChannelBase = class {
  static {
    __name(this, "ChannelBase");
  }
  config;
  bridge;
  groupGate;
  gate;
  router;
  name;
  /** Resolved proxy URL, available to subclasses for adapter-specific clients. */
  proxy;
  channelMemory;
  groupHistory;
  loopController;
  instructedSessions = /* @__PURE__ */ new Set();
  commands = /* @__PURE__ */ new Map();
  /** Per-session promise chain to serialize prompt + send (followup mode). */
  sessionQueues = /* @__PURE__ */ new Map();
  registerBridgeEvents;
  /**
   * Per-session generation, bumped by /clear. A queued followup turn captures the
   * generation when it enqueues and bails if /clear bumped it before the turn ran,
   * so a cleared session can't be resurrected by an already-queued prompt.
   */
  sessionGenerations = /* @__PURE__ */ new Map();
  /** Per-session active prompt tracking for dispatch modes. */
  activePrompts = /* @__PURE__ */ new Map();
  /** Per-session message buffer for collect mode. */
  collectBuffers = /* @__PURE__ */ new Map();
  bridgeToolCallListener = /* @__PURE__ */ __name((event) => {
    const target = this.router.getTarget(event.sessionId);
    if (target) {
      this.onToolCall(target.chatId, event);
    }
  }, "bridgeToolCallListener");
  bridgeSessionDiedListener = /* @__PURE__ */ __name((event) => {
    this.onSessionDied(event.sessionId);
  }, "bridgeSessionDiedListener");
  constructor(name, config, bridge, options) {
    this.name = name;
    this.config = config;
    this.bridge = bridge;
    this.proxy = options?.proxy;
    this.channelMemory = options?.channelMemory;
    this.groupHistory = new GroupHistoryStore(options?.groupHistoryPath ?? join4(getGlobalQwenDir(), "channels", `${encodeURIComponent(name)}-group-history.jsonl`));
    this.loopController = options?.loopController;
    this.groupGate = new GroupGate(config.groupPolicy, config.groups);
    const pairingStore = config.senderPolicy === "pairing" ? new PairingStore(name) : void 0;
    this.gate = new SenderGate(config.senderPolicy, config.allowedUsers, pairingStore);
    this.router = options?.router || new SessionRouter(bridge, config.cwd, config.sessionScope);
    this.registerSharedCommands();
    this.registerBridgeEvents = options?.registerBridgeEvents ?? !options?.router;
    if (this.registerBridgeEvents) {
      this.attachBridgeEvents(bridge);
    }
  }
  supportsProactiveSend() {
    return false;
  }
  supportsProactiveTarget(target) {
    return target.threadId === void 0;
  }
  async pushProactive(target, text) {
    if (target.threadId) {
      throw new Error("Channel does not support proactive loop messages for threaded targets.");
    }
    await this.sendMessage(target.chatId, text);
  }
  /** Replace the bridge instance (used after crash recovery restart). */
  setBridge(bridge) {
    if (this.registerBridgeEvents) {
      this.detachBridgeEvents(this.bridge);
    }
    this.router.setBridge(bridge);
    this.bridge = bridge;
    if (this.registerBridgeEvents) {
      this.attachBridgeEvents(bridge);
    }
  }
  async runLoopPrompt(job, options = {}) {
    if (!this.supportsProactiveSend()) {
      throw new Error("Channel does not support proactive loop messages.");
    }
    if (this.config.sessionScope === "single") {
      await this.loopController?.disable(job.id);
      throw new Error("Loop messages are not supported with single session scope.");
    }
    if (job.channelName !== this.name) {
      throw new Error(`Loop ${job.id} belongs to ${job.channelName}, not ${this.name}.`);
    }
    if (!this.supportsProactiveTarget(job.target)) {
      throw new Error("Channel does not support proactive loop messages for this chat target.");
    }
    if (!this.isStoredLoopTargetAuthorized(job.target, job.createdBy)) {
      await this.loopController?.disable(job.id);
      throw new Error(`Loop ${job.id} target is no longer authorized.`);
    }
    const sessionId = await this.router.resolve(this.name, job.target.senderId, job.target.chatId, job.target.threadId, job.cwd);
    const label = sanitizeQuotedText(job.label || job.id, 80);
    const createdBy = sanitizeSenderName(job.createdBy || "unknown");
    let promptText = `[Loop "${label}" created by ${createdBy}]

${sanitizePromptText(job.prompt)}`;
    const shouldPrependSessionContext = !this.instructedSessions.has(sessionId);
    const prev = this.sessionQueues.get(sessionId) ?? Promise.resolve();
    const generation = this.sessionGenerations.get(sessionId) ?? 0;
    const current = prev.then(async () => {
      if ((this.sessionGenerations.get(sessionId) ?? 0) !== generation) {
        process.stderr.write(`[${this.name}] dropped loop ${job.id} for session ${sessionId}: session was cleared before it ran
`);
        throw new ChannelLoopSkippedError("loop dropped because session was cleared before it ran");
      }
      if (options.shouldContinue && !await options.shouldContinue()) {
        throw new ChannelLoopSkippedError("loop dropped because it is no longer enabled");
      }
      let shouldClaimSessionContext = false;
      if (shouldPrependSessionContext) {
        const context = [];
        let sessionContextReady = true;
        if (this.channelMemory && this.isSenderAuthorizedForChannelMemory(job.target.senderId) && (!this.isSharedSessionTarget(job.target) || this.config.senderPolicy === "allowlist")) {
          try {
            const memoryText = (await this.channelMemory.readChannelMemory({
              channelName: this.name,
              chatId: job.target.chatId,
              threadId: job.target.threadId
            })).trim();
            if (memoryText) {
              context.push(`Channel memory for this chat:
${sanitizePromptText(memoryText)}`);
            }
          } catch (error) {
            process.stderr.write(`[${this.name}] channel memory read failed for loop ${job.id} chat ${sanitizeLogText(job.target.chatId, 64)}: ${sanitizeLogText(this.channelMemoryErrorMessage(error), 200)}
`);
            this.instructedSessions.delete(sessionId);
            sessionContextReady = false;
          }
        }
        if (this.config.instructions) {
          context.push(this.config.instructions);
        }
        if (context.length > 0) {
          promptText = `${context.join("\n\n")}

${promptText}`;
        }
        if (sessionContextReady) {
          shouldClaimSessionContext = true;
        }
      }
      if ((this.sessionGenerations.get(sessionId) ?? 0) !== generation) {
        process.stderr.write(`[${this.name}] dropped loop ${job.id} for session ${sessionId}: session was cleared before it ran
`);
        throw new ChannelLoopSkippedError("loop dropped because session was cleared before it ran");
      }
      if (shouldClaimSessionContext) {
        this.instructedSessions.add(sessionId);
      }
      let doneResolve = /* @__PURE__ */ __name(() => {
      }, "doneResolve");
      const done = new Promise((resolve2) => {
        doneResolve = resolve2;
      });
      const promptState = {
        cancelled: false,
        done,
        resolve: doneResolve,
        chatId: job.target.chatId,
        messageId: job.id
      };
      this.activePrompts.set(sessionId, promptState);
      this.onPromptStart(job.target.chatId, sessionId);
      const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
        if (sid === sessionId && !promptState.cancelled) {
          this.onResponseChunk(job.target.chatId, chunk, sessionId);
        }
      }, "onChunk");
      const promptBridge = this.bridge;
      promptBridge.on("textChunk", onChunk);
      try {
        const response = await this.runLoopBridgePrompt(promptBridge, sessionId, promptText, promptState, job.id, options.timeoutMs);
        if (promptState.cancelRequested && !promptState.cancelled) {
          const cancelled = await promptState.cancelRequested;
          if (cancelled) {
            promptState.cancelled = true;
          }
        }
        if (promptState.cancelled) {
          throw new ChannelLoopSkippedError("loop cancelled before delivery");
        }
        if (options.shouldContinue && !await options.shouldContinue()) {
          throw new ChannelLoopSkippedError("loop dropped before delivery");
        }
        if (response) {
          await this.pushProactive(job.target, response);
        }
        return response;
      } finally {
        promptBridge.off("textChunk", onChunk);
        const stillCurrent = this.activePrompts.get(sessionId) === promptState;
        if (!promptState.clearEvicted) {
          try {
            this.onPromptEnd(job.target.chatId, sessionId);
          } catch (err) {
            process.stderr.write(`[${this.name}] onPromptEnd threw in loop ${job.id} for session ${sessionId}: ${err instanceof Error ? err.message : err}
`);
          }
        }
        if (stillCurrent) {
          this.activePrompts.delete(sessionId);
        }
        promptState.resolve();
        const buffer = this.collectBuffers.get(sessionId);
        if (stillCurrent && buffer && buffer.length > 0) {
          this.collectBuffers.delete(sessionId);
          const lost = buffer.length;
          const coalesced = buffer.map((b) => b.text).join("\n\n");
          const lastEnvelope = buffer[buffer.length - 1].envelope;
          const syntheticEnvelope = {
            ...lastEnvelope,
            text: coalesced,
            alreadyPrefixed: true,
            referencedText: void 0,
            attachments: void 0,
            imageBase64: void 0,
            imageMimeType: void 0
          };
          this.handleInbound(syntheticEnvelope).catch((err) => {
            process.stderr.write(`[${this.name}] dropped ${lost} buffered message(s) after loop ${job.id} for session ${sessionId} (last sender ${lastEnvelope.senderId}): ${err instanceof Error ? err.message : String(err)}
`);
          });
        }
      }
    });
    this.sessionQueues.set(sessionId, current.then(() => void 0).catch(() => {
    }));
    return current;
  }
  async runLoopBridgePrompt(promptBridge, sessionId, promptText, promptState, jobId, timeoutMs) {
    const prompt = promptBridge.prompt(sessionId, promptText, {});
    prompt.catch(() => {
    });
    if (timeoutMs === void 0) {
      return prompt;
    }
    let timer;
    try {
      return await Promise.race([
        prompt,
        new Promise((_, reject) => {
          timer = setTimeout(() => {
            reject(new Error("loop timed out"));
          }, timeoutMs);
          timer.unref?.();
        })
      ]);
    } catch (err) {
      if (err instanceof Error && err.message === "loop timed out") {
        promptState.cancelled = true;
        await this.cancelTimedOutLoopPrompt(promptBridge, sessionId, jobId);
      }
      throw err;
    } finally {
      clearTimeout(timer);
    }
  }
  async cancelTimedOutLoopPrompt(promptBridge, sessionId, jobId) {
    let graceTimer;
    try {
      const cancelled = await Promise.race([
        promptBridge.cancelSession(sessionId).then(() => true),
        new Promise((resolve2) => {
          graceTimer = setTimeout(() => resolve2(false), LOOP_CANCEL_GRACE_MS);
          graceTimer.unref?.();
        })
      ]);
      if (!cancelled) {
        this.router.removeSessionId(sessionId);
        this.instructedSessions.delete(sessionId);
        process.stderr.write(`[${this.name}] retired timed out loop ${jobId} session ${sessionId} after cancel did not settle
`);
      }
    } catch (cancelErr) {
      process.stderr.write(`[${this.name}] cancelSession failed for timed out loop ${jobId} in session ${sessionId}: ${cancelErr instanceof Error ? cancelErr.message : cancelErr}
`);
    } finally {
      clearTimeout(graceTimer);
    }
  }
  onToolCall(_chatId, _event) {
  }
  onSessionDied(sessionId) {
    this.router.removeSessionId(sessionId);
    this.instructedSessions.delete(sessionId);
  }
  attachBridgeEvents(bridge) {
    bridge.on("toolCall", this.bridgeToolCallListener);
    bridge.on("sessionDied", this.bridgeSessionDiedListener);
  }
  detachBridgeEvents(bridge) {
    bridge.off("toolCall", this.bridgeToolCallListener);
    bridge.off("sessionDied", this.bridgeSessionDiedListener);
  }
  /**
   * Called when a prompt actually begins processing (inside the session queue).
   * Override to show a platform-specific working indicator (e.g., typing, reaction).
   * Not called for buffered messages (collect mode) or gated/blocked messages.
   */
  onPromptStart(_chatId, _sessionId, _messageId) {
  }
  /**
   * Called when a prompt finishes (response sent or cancelled).
   * Override to hide the working indicator.
   */
  onPromptEnd(_chatId, _sessionId, _messageId) {
  }
  /**
   * Called for each text chunk as the agent streams its response.
   * Override to implement progressive display (e.g., updating an AI card in-place).
   * Default: no-op (chunks are collected internally and delivered via onResponseComplete).
   */
  onResponseChunk(_chatId, _chunk, _sessionId) {
  }
  /**
   * Called when the agent's full response is ready.
   * Override to customize delivery (e.g., finalize an AI card).
   * Default: calls sendMessage() with the full response text.
   */
  async onResponseComplete(chatId, fullText, _sessionId) {
    await this.sendMessage(chatId, fullText);
  }
  /**
   * Register a slash command handler. Subclasses can call this to add
   * platform-specific commands (e.g., /start for Telegram).
   * Overrides shared commands if the same name is registered.
   */
  registerCommand(name, handler) {
    this.commands.set(name.toLowerCase(), handler);
  }
  registerCancelCommand(name = "cancel") {
    this.registerCommand(name, async (envelope) => {
      if (!this.isAuthorizedForSharedSession(envelope)) {
        await this.sendMessage(envelope.chatId, "Only authorized members can cancel requests in this shared session.");
        return true;
      }
      const activeSessionId = this.findActiveSessionId(envelope);
      if (!activeSessionId) {
        await this.sendMessage(envelope.chatId, "No request is currently running.");
        return true;
      }
      const active = this.activePrompts.get(activeSessionId);
      if (!active) {
        await this.sendMessage(envelope.chatId, "No request is currently running.");
        return true;
      }
      const cancelRequested = active.cancelRequested ?? this.bridge.cancelSession(activeSessionId).then(() => true, (err) => {
        process.stderr.write(`[${this.name}] cancelSession failed for session=${activeSessionId}: ${err instanceof Error ? err.message : err}
`);
        active.cancelRequested = void 0;
        return false;
      });
      active.cancelRequested = cancelRequested;
      const cancelSucceeded = await cancelRequested;
      if (!cancelSucceeded) {
        await this.sendMessage(envelope.chatId, "Failed to cancel current request.");
        return true;
      }
      active.cancelled = true;
      active.stopStreaming?.();
      this.collectBuffers.delete(activeSessionId);
      await this.sendMessage(envelope.chatId, "Cancelled current request.");
      return true;
    });
  }
  /** Register shared slash commands. Called from constructor. */
  registerSharedCommands() {
    const doClear = /* @__PURE__ */ __name(async (envelope) => {
      const removedIds = this.router.removeSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
      this.clearPendingGroupHistory(envelope);
      if (removedIds.length > 0) {
        for (const id of removedIds) {
          if (this.isSharedSession(envelope)) {
            const who = sanitizeSenderName(envelope.senderName || envelope.senderId || "unknown");
            process.stderr.write(`[${this.name}] shared session ${id} cleared by ${who} (sender ${envelope.senderId})
`);
          }
          this.sessionGenerations.set(id, (this.sessionGenerations.get(id) ?? 0) + 1);
          const active = this.activePrompts.get(id);
          this.collectBuffers.delete(id);
          if (active) {
            const settled = await this.cancelAndAwaitActive(active, id);
            if (!settled) {
              const wedgedChat = active.chatId ? sanitizeLogText(active.chatId, 64) : "unknown";
              const wedgedMessage = active.messageId ? `, message ${sanitizeLogText(active.messageId, 64)}` : "";
              process.stderr.write(`[${this.name}] /clear abandoned a wedged turn for session ${id} (chat ${wedgedChat}${wedgedMessage}): it did not wind down within ${CLEAR_CANCEL_TIMEOUT_MS}ms
`);
              active.clearEvicted = true;
              try {
                this.onPromptEnd(active.chatId, id, active.messageId);
              } catch (err) {
                process.stderr.write(`[${this.name}] onPromptEnd threw during /clear eviction for session ${id}: ${err instanceof Error ? err.message : err}
`);
              }
            }
          }
          this.instructedSessions.delete(id);
          const drained = this.sessionQueues.get(id);
          const bumpedGeneration = this.sessionGenerations.get(id);
          this.sessionQueues.delete(id);
          this.activePrompts.delete(id);
          if (drained) {
            void drained.then(() => {
              if (!this.sessionQueues.has(id) && this.sessionGenerations.get(id) === bumpedGeneration) {
                this.sessionGenerations.delete(id);
              }
            });
          } else {
            this.sessionGenerations.delete(id);
          }
        }
        await this.sendMessage(envelope.chatId, "Session cleared. The next message starts a fresh conversation.");
      } else {
        await this.sendMessage(envelope.chatId, "No active session to clear.");
      }
    }, "doClear");
    const clearHandler = /* @__PURE__ */ __name(async (envelope, args) => {
      if (!this.isAuthorizedForSharedSession(envelope)) {
        await this.sendMessage(envelope.chatId, "Only authorized members can clear this shared session.");
        return true;
      }
      if (this.isSharedSession(envelope) && args.toLowerCase() !== "confirm") {
        await this.sendMessage(envelope.chatId, 'This clears the shared session for everyone who shares it. Re-send with "confirm" (e.g. /clear confirm) to proceed.');
        return true;
      }
      await doClear(envelope);
      return true;
    }, "clearHandler");
    this.registerCommand("clear", clearHandler);
    this.registerCommand("reset", clearHandler);
    this.registerCommand("new", clearHandler);
    this.registerCommand("who", async (envelope) => {
      if (!this.isAuthorizedForSharedSession(envelope)) {
        await this.sendMessage(envelope.chatId, "Only authorized members can view this shared session.");
        return true;
      }
      const active = this.router.hasSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
      const scopeNote = this.config.sessionScope === "single" ? " (shared channel-wide)" : this.isSharedSession(envelope) ? envelope.isGroup ? " (shared by this group)" : "" : envelope.isGroup ? " (private to you)" : "";
      await this.sendMessage(envelope.chatId, [
        `Channel: ${this.name}`,
        // Only the basename — don't leak the absolute cwd to group members.
        `Workspace: ${basename(this.config.cwd)}`,
        `Session: ${active ? "active" : "none"}${scopeNote}`
      ].join("\n"));
      return true;
    });
    this.registerCommand("remember-channel", async (envelope, args) => {
      if (!await this.ensureChannelMemoryAuthorized(envelope)) {
        return true;
      }
      if (envelope.isGroup) {
        await this.sendMessage(envelope.chatId, "Channel memory cannot be changed in group chats.");
        return true;
      }
      if (args.trim() === "") {
        await this.sendMessage(envelope.chatId, "Usage: /remember-channel <text>");
        return true;
      }
      const channelMemory = await this.getChannelMemory(envelope);
      if (!channelMemory) {
        return true;
      }
      try {
        await channelMemory.appendChannelMemory(this.channelMemoryTarget(envelope), args.trim());
      } catch (error) {
        const message = this.channelMemoryErrorMessage(error);
        this.logChannelMemoryError("save", envelope, message);
        await this.sendMessage(envelope.chatId, `Failed to save channel memory: ${this.channelMemoryUserErrorMessage()}`);
        return true;
      }
      this.invalidateSessionContext(envelope);
      await this.sendMessage(envelope.chatId, "Channel memory updated.");
      return true;
    });
    this.registerCommand("channel-memory", async (envelope) => {
      if (!await this.ensureChannelMemoryAuthorized(envelope)) {
        return true;
      }
      if (envelope.isGroup) {
        await this.sendMessage(envelope.chatId, "Channel memory cannot be shown in group chats.");
        return true;
      }
      const channelMemory = await this.getChannelMemory(envelope);
      if (!channelMemory) {
        return true;
      }
      let text;
      try {
        text = (await channelMemory.readChannelMemory(this.channelMemoryTarget(envelope))).trim();
      } catch (error) {
        const message = this.channelMemoryErrorMessage(error);
        this.logChannelMemoryError("read", envelope, message);
        await this.sendMessage(envelope.chatId, `Failed to read channel memory: ${this.channelMemoryUserErrorMessage()}`);
        return true;
      }
      await this.sendMessage(envelope.chatId, text === "" ? "No channel memory saved." : sanitizePromptText(text));
      return true;
    });
    this.registerCommand("forget-channel", async (envelope, args) => {
      if (!await this.ensureChannelMemoryAuthorized(envelope)) {
        return true;
      }
      if (envelope.isGroup) {
        await this.sendMessage(envelope.chatId, "Channel memory cannot be changed in group chats.");
        return true;
      }
      if (args.toLowerCase() !== "confirm") {
        await this.sendMessage(envelope.chatId, 'This clears channel memory for this chat. Re-send with "confirm" (e.g. /forget-channel confirm) to proceed.');
        return true;
      }
      const channelMemory = await this.getChannelMemory(envelope);
      if (!channelMemory) {
        return true;
      }
      let result;
      try {
        result = await channelMemory.clearChannelMemory(this.channelMemoryTarget(envelope));
      } catch (error) {
        const message = this.channelMemoryErrorMessage(error);
        this.logChannelMemoryError("clear", envelope, message);
        await this.sendMessage(envelope.chatId, `Failed to clear channel memory: ${this.channelMemoryUserErrorMessage()}`);
        return true;
      }
      this.invalidateSessionContext(envelope);
      await this.sendMessage(envelope.chatId, result.changed ? "Channel memory cleared." : "No channel memory saved.");
      return true;
    });
    this.registerCommand("help", async (envelope) => {
      const lines = [
        "Commands:",
        "/help \u2014 Show this help",
        this.isSharedSession(envelope) ? "/clear confirm \u2014 Clear the shared session (aliases: /reset, /new)" : "/clear \u2014 Clear your session (aliases: /reset, /new)",
        "/who \u2014 Show current session & workspace",
        "/status \u2014 Show session info",
        "/remember-channel <text> \u2014 Save memory for this chat",
        "/channel-memory \u2014 Show memory for this chat",
        "/forget-channel confirm \u2014 Clear memory for this chat"
      ];
      const sharedCmds = /* @__PURE__ */ new Set([
        "help",
        "clear",
        "reset",
        "new",
        "who",
        "status",
        "remember-channel",
        "channel-memory",
        "forget-channel"
      ]);
      const platformCmds = [...this.commands.keys()].filter((c) => !sharedCmds.has(c));
      if (platformCmds.length > 0) {
        for (const cmd of platformCmds) {
          lines.push(`/${cmd}`);
        }
      }
      const sessionId = this.router.getSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
      const agentCommands = sessionId ? this.getAgentCommandsForSession(sessionId) : this.bridge.availableCommands;
      if (agentCommands.length > 0) {
        lines.push("", "Agent commands (forwarded to Qwen Code):");
        for (const cmd of agentCommands) {
          lines.push(`/${cmd.name} \u2014 ${cmd.description}`);
        }
      }
      lines.push("", "Send any text to chat with the agent.");
      await this.sendMessage(envelope.chatId, lines.join("\n"));
      return true;
    });
    this.registerCommand("status", async (envelope) => {
      if (!this.isAuthorizedForSharedSession(envelope)) {
        await this.sendMessage(envelope.chatId, "Only authorized members can view this shared session.");
        return true;
      }
      const hasSession = this.router.hasSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
      const policy = this.config.senderPolicy;
      const lines = [
        `Session: ${hasSession ? "active" : "none"}`,
        `Access: ${policy}`,
        `Channel: ${this.name}`
      ];
      await this.sendMessage(envelope.chatId, lines.join("\n"));
      return true;
    });
    this.registerCommand("loop", async (envelope, args) => this.handleLoopCommand(envelope, args));
  }
  async handleLoopCommand(envelope, args) {
    if (!this.loopController) {
      await this.sendMessage(envelope.chatId, "Loops are not available.");
      return true;
    }
    if (!this.isAuthorizedForSharedSession(envelope)) {
      await this.sendMessage(envelope.chatId, "Only authorized members can use loops in this shared session.");
      return true;
    }
    const [subcommand = "", ...rest] = args.trim().split(/\s+/u);
    switch (subcommand.toLowerCase()) {
      case "add":
        return this.handleLoopAdd(envelope, rest.join(" "));
      case "list":
        return this.handleLoopList(envelope);
      case "inspect":
        return this.handleLoopInspect(envelope, rest[0]);
      case "cancel":
        return this.handleLoopCancel(envelope, rest[0]);
      default:
        await this.sendMessage(envelope.chatId, 'Usage: /loop add "<cron>" <prompt> | /loop list | /loop inspect <id> | /loop cancel <id>');
        return true;
    }
  }
  async handleLoopAdd(envelope, args) {
    if (!this.loopController)
      return true;
    if (!this.supportsProactiveSend()) {
      await this.sendMessage(envelope.chatId, "This channel does not support proactive loop messages.");
      return true;
    }
    if (this.config.sessionScope === "single") {
      await this.sendMessage(envelope.chatId, "Loops are not supported when sessionScope is single.");
      return true;
    }
    const parsed = parseLoopAddArgs(args);
    if (!parsed) {
      await this.sendMessage(envelope.chatId, 'Usage: /loop add "<cron>" <prompt>');
      return true;
    }
    try {
      this.loopController.validateCron(parsed.cron);
    } catch (err) {
      await this.sendMessage(envelope.chatId, `Invalid cron expression: ${err instanceof Error ? err.message : String(err)}`);
      return true;
    }
    const target = this.loopTargetFromEnvelope(envelope);
    if (!this.supportsProactiveTarget(target)) {
      await this.sendMessage(envelope.chatId, "This channel does not support proactive loop messages for this chat target.");
      return true;
    }
    const prompt = sanitizePromptText(parsed.prompt.trim());
    if (Array.from(prompt).length > MAX_LOOP_PROMPT_CHARS) {
      await this.sendMessage(envelope.chatId, `Loop prompt is too long; keep it under ${MAX_LOOP_PROMPT_CHARS} characters.`);
      return true;
    }
    const input = {
      channelName: this.name,
      target,
      cwd: this.config.cwd,
      cron: parsed.cron,
      prompt,
      label: truncateLoopLabel(prompt),
      recurring: true,
      createdBy: sanitizeSenderName(envelope.senderName || envelope.senderId || "unknown")
    };
    let job;
    if (this.loopController.createForTarget) {
      job = await this.loopController.createForTarget(input, MAX_LOOP_JOBS_PER_TARGET);
    } else {
      const existingJobs = await this.loopController.listForTarget(this.name, target);
      if (existingJobs.filter((existingJob) => existingJob.enabled).length < MAX_LOOP_JOBS_PER_TARGET) {
        job = await this.loopController.create(input);
      }
    }
    if (!job) {
      await this.sendMessage(envelope.chatId, `Too many loops for this chat. Cancel an existing loop before adding another.`);
      return true;
    }
    await this.sendMessage(envelope.chatId, `Loop ${job.id}: ${job.cron}`);
    return true;
  }
  async handleLoopList(envelope) {
    if (!this.loopController)
      return true;
    const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
    if (jobs.length === 0) {
      await this.sendMessage(envelope.chatId, "No loops.");
      return true;
    }
    await this.sendMessage(envelope.chatId, jobs.map((job) => this.formatLoopListLine(job)).join("\n"));
    return true;
  }
  async handleLoopInspect(envelope, id) {
    if (!this.loopController)
      return true;
    if (!id) {
      await this.sendMessage(envelope.chatId, "Usage: /loop inspect <id>");
      return true;
    }
    const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
    const job = jobs.find((candidate) => candidate.id === id);
    if (!job) {
      await this.sendMessage(envelope.chatId, `No loop ${id}.`);
      return true;
    }
    const lines = [
      `Loop ${job.id}`,
      `Status: ${job.enabled ? "enabled" : "disabled"}, last=${this.lastLoopStatus(job)}`,
      `Cron: ${job.cron}`,
      `Next: ${this.formatNextFireTime(job)}`,
      `Runs: ${job.runCount}`,
      `Created by: ${job.createdBy}`,
      `Created: ${job.createdAt}`
    ];
    if (job.lastFinishedAt) {
      lines.push(`Last finished: ${job.lastFinishedAt}`);
    }
    if (job.lastError) {
      lines.push(`Last error: ${job.lastError}`);
    }
    if (job.lastResultPreview) {
      lines.push(`Last result: ${job.lastResultPreview}`);
    }
    lines.push(`Prompt: ${job.prompt}`);
    await this.sendMessage(envelope.chatId, lines.join("\n"));
    return true;
  }
  formatLoopListLine(job) {
    const fields = [
      job.id,
      job.cron,
      job.enabled ? "enabled" : "disabled",
      `last=${this.lastLoopStatus(job)}`,
      `next=${this.formatNextFireTime(job)}`,
      `runs=${job.runCount}`
    ];
    if (job.label)
      fields.push(job.label);
    return fields.join(" ");
  }
  lastLoopStatus(job) {
    if (job.runningSince)
      return "running";
    return job.lastStatus ?? "never";
  }
  formatNextFireTime(job) {
    try {
      return this.loopController?.nextFireTime?.(job).toISOString() ?? "n/a";
    } catch {
      return "invalid cron";
    }
  }
  async handleLoopCancel(envelope, id) {
    if (!this.loopController)
      return true;
    if (!id) {
      await this.sendMessage(envelope.chatId, "Usage: /loop cancel <id>");
      return true;
    }
    const jobs = await this.loopController.listForTarget(this.name, this.loopTargetFromEnvelope(envelope));
    const match = jobs.find((job) => job.id === id);
    const disabled = match ? await this.loopController.disable(id) : false;
    await this.sendMessage(envelope.chatId, disabled ? `Cancelled loop ${id}.` : `No loop ${id}.`);
    return true;
  }
  loopTargetFromEnvelope(envelope) {
    return {
      channelName: this.name,
      senderId: envelope.senderId,
      chatId: envelope.chatId,
      threadId: envelope.threadId,
      isGroup: envelope.isGroup === true
    };
  }
  isStoredLoopTargetAuthorized(target, senderName) {
    if (target.isGroup === void 0) {
      return false;
    }
    const envelope = {
      channelName: this.name,
      senderId: target.senderId,
      senderName,
      chatId: target.chatId,
      text: "",
      threadId: target.threadId,
      isGroup: target.isGroup === true,
      isMentioned: true,
      isReplyToBot: true
    };
    return this.groupGate.check(envelope).allowed && this.gate.isAllowed(target.senderId) && this.isAuthorizedForSharedSession(envelope);
  }
  /** Check if a message text matches a registered local command. */
  isLocalCommand(text) {
    const parsed = this.parseCommand(text);
    return parsed !== null && this.commands.has(parsed.command);
  }
  findActiveSessionId(envelope) {
    const sessionId = this.router.getSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
    return sessionId && this.activePrompts.has(sessionId) ? sessionId : void 0;
  }
  channelMemoryTarget(envelope) {
    return {
      channelName: this.name,
      chatId: envelope.chatId,
      threadId: envelope.threadId
    };
  }
  invalidateSessionContext(envelope) {
    const sessionId = this.router.getSession(this.name, envelope.senderId, envelope.chatId, envelope.threadId);
    if (sessionId) {
      this.instructedSessions.delete(sessionId);
    }
  }
  dropQueuedTurnIfStale(sessionId, generation, envelope) {
    if ((this.sessionGenerations.get(sessionId) ?? 0) === generation) {
      return false;
    }
    const loggedText = sanitizeLogText(envelope.text, 80);
    process.stderr.write(`[${this.name}] dropped queued turn from ${envelope.senderId} for session ${sessionId}: session was cleared before it ran (text: ${loggedText})
`);
    return true;
  }
  isAuthorizedForChannelMemory(envelope) {
    return this.isSenderAuthorizedForChannelMemory(envelope.senderId);
  }
  isSenderAuthorizedForChannelMemory(senderId) {
    return this.config.allowedUsers.length > 0 && this.config.allowedUsers.includes(senderId);
  }
  async ensureChannelMemoryAuthorized(envelope) {
    if (!this.isAuthorizedForChannelMemory(envelope)) {
      await this.sendMessage(envelope.chatId, "Only authorized members can manage channel memory.");
      return false;
    }
    return true;
  }
  async getChannelMemory(envelope) {
    if (!this.channelMemory) {
      await this.sendMessage(envelope.chatId, "Channel memory is not configured for this channel.");
      return void 0;
    }
    return this.channelMemory;
  }
  channelMemoryErrorMessage(error) {
    return error instanceof Error ? error.message : String(error);
  }
  channelMemoryUserErrorMessage() {
    return "An error occurred while accessing channel memory.";
  }
  logChannelMemoryError(action, envelope, message) {
    process.stderr.write(`[${this.name}] channel memory ${action} failed for sender=${sanitizeLogText(envelope.senderId, 80)} chat=${sanitizeLogText(envelope.chatId, 80)} thread=${sanitizeLogText(envelope.threadId ?? "", 80)}: ${sanitizeLogText(message, 200)}
`);
  }
  /**
   * Whether the resolved session is SHARED across senders. `single` collapses
   * the whole channel to one `__single__` session for EVERY sender — group OR
   * DM — so it is ALWAYS shared (even a DM maps to `__single__`). `thread` is
   * shared only in a group (a DM maps to the lone caller's own chat). `user` is
   * per-sender, never shared. Drives both the destructive-/clear confirm gate
   * and the host-shell (`!`) gate.
   */
  isSharedSession(envelope) {
    return this.isSharedSessionTarget(envelope);
  }
  isSharedSessionTarget(target) {
    return this.config.sessionScope === "single" || target.isGroup === true && this.config.sessionScope === "thread";
  }
  /**
   * Whether `envelope.senderId` may act on the resolved session's destructive or
   * workspace-leaking commands (/clear, /who). A SHARED session with a non-empty
   * allowedUsers list is restricted to those members; a per-user session, or one
   * with no allowlist, is unrestricted. Shared verbatim by /clear and /who so the
   * gate can't drift; each caller sends its own rejection wording.
   */
  isAuthorizedForSharedSession(envelope) {
    if (!this.isSharedSession(envelope))
      return true;
    const authorized = this.config.allowedUsers;
    return authorized.length === 0 || authorized.includes(envelope.senderId);
  }
  stopActiveStreaming(active, sessionId, reason) {
    try {
      active.stopStreaming?.();
    } catch (err) {
      process.stderr.write(`[${this.name}] stopStreaming threw during ${reason} for session ${sessionId}: ${err instanceof Error ? err.message : err}
`);
    }
  }
  /**
   * Cancel the active turn and wait (bounded) for it to wind down. Stops the
   * BlockStreamer so buffered text can't leak via the idle timer, then fires a
   * best-effort cancelSession (NOT awaited — a wedged child/daemon can leave the
   * request pending forever). Returns true if active.done settled first, false
   * if the CLEAR_CANCEL_TIMEOUT_MS bound won (the turn never wound down). Used by
   * /clear, which genuinely EVICTS the session and so must proceed even when the
   * turn is wedged. Steer no longer uses this: it best-effort cancels then chains
   * the new turn behind the old one (see handleInbound), so it never needs to
   * proceed past a still-active turn.
   */
  async cancelAndAwaitActive(active, sessionId) {
    active.cancelled = true;
    this.stopActiveStreaming(active, sessionId, "cancel");
    void this.bridge.cancelSession(sessionId).catch((err) => {
      process.stderr.write(`[${this.name}] cancelSession failed for session=${sessionId} (clear/await): ${err instanceof Error ? err.message : err}
`);
    });
    let timer;
    const settled = await Promise.race([
      active.done.then(() => true),
      new Promise((resolve2) => {
        timer = setTimeout(() => resolve2(false), CLEAR_CANCEL_TIMEOUT_MS);
      })
    ]);
    clearTimeout(timer);
    return settled;
  }
  /**
   * Parse a slash command from message text.
   * Returns { command, raw, args } or null if not a slash command. `command` is
   * lowercased for case-insensitive LOCAL dispatch (registerCommand lowercases the
   * names it stores); `raw` keeps the typed case so agent-command matching can be
   * CASE-SENSITIVE, mirroring the CLI's parseSlashCommand (`cmd.name === part`).
   */
  parseCommand(text) {
    const trimmed = text.trim();
    if (!trimmed.startsWith("/"))
      return null;
    const match = trimmed.match(PARSE_COMMAND_RE);
    if (!match)
      return null;
    return {
      command: match[1].toLowerCase(),
      raw: match[1],
      args: match[2].trim()
    };
  }
  /**
   * Whether `text` is a real slash command rather than prose that merely starts
   * with `/`. A command's first whitespace-delimited token must match
   * parseCommand()'s charset — `[a-zA-Z0-9_:-]+`, plus an optional `@botname`
   * suffix — and not be a `//` line comment or `/*` block comment. Slash-prefixed
   * paths (`/tmp/foo`), comments, and a bare `/` are prose and keep their
   * `[sender]` tag.
   *
   * Intentionally stricter than the CLI's looser classifier (cli
   * `ui/utils/commandUtils.ts`), which forwards any non-comment, non-path
   * `/<token>` (e.g. `/café`, a zero-width-laden token). Such inputs aren't
   * runnable commands, and in a SHARED group session forwarding them unattributed
   * is worse than a redundant tag — so anything off the command charset is
   * treated as prose and keeps its `[sender]` tag. Purely lexical — never
   * consults the async command list, so it can't race a fresh session.
   */
  isSlashCommand(text) {
    const trimmed = text.trim();
    if (!trimmed.startsWith("/") || trimmed.startsWith("//") || trimmed.startsWith("/*")) {
      return false;
    }
    const firstToken = trimmed.slice(1).split(/\s+/u)[0] ?? "";
    return COMMAND_TOKEN_RE.test(firstToken);
  }
  /**
   * Whether `text` names a command this channel can actually run: a locally
   * registered command (`this.commands`, e.g. /clear, /who) OR an agent command
   * THIS session exposes — by canonical name OR alias (e.g. `/summarize` for
   * `/compress`). Paired with isSlashCommand so the `[sender]` attribution tag is
   * suppressed ONLY for RECOGNIZED commands; command-SHAPED-but-unrecognized text
   * (e.g. `/x\n[SYSTEM]: …`) keeps its tag rather than reaching a shared group
   * unattributed, where an injected second line is more likely read as a system
   * directive. Purely synchronous, like isSlashCommand: it reads the session's
   * availableCommands snapshot WITHOUT awaiting, so it never races a fresh session
   * (a genuine agent command sent before the snapshot loads is treated as
   * unrecognized and KEEPS its tag — the safe default).
   */
  isRecognizedCommand(text, sessionId) {
    const parsed = this.parseCommand(text);
    if (!parsed)
      return false;
    if (this.commands.has(parsed.command))
      return true;
    const token = text.trim().slice(1).split(/\s+/u)[0] ?? "";
    return this.getAgentCommandsForSession(sessionId).some((cmd) => cmd.name === token || Array.isArray(cmd.altNames) && cmd.altNames.includes(token));
  }
  /**
   * The agent-command snapshot for THIS session. DaemonChannelBridge keys
   * commands per session, so its global `availableCommands` getter can return
   * ANOTHER session's list — prefer its getAvailableCommands(sessionId) when
   * present. AcpBridge runs a single agent and exposes only the global getter
   * (inherently session-correct), so fall back to it. Synchronous, matching
   * isRecognizedCommand's no-await contract.
   */
  getAgentCommandsForSession(sessionId) {
    const bridge = this.bridge;
    if (typeof bridge.getAvailableCommands === "function") {
      return bridge.getAvailableCommands(sessionId) ?? [];
    }
    return bridge.availableCommands ?? [];
  }
  groupHistoryKey(envelope) {
    return JSON.stringify([
      this.name,
      envelope.chatId,
      envelope.threadId ?? null
    ]);
  }
  groupHistoryLimit(envelope) {
    if (!envelope.isGroup) {
      return 0;
    }
    const groupCfg = this.config.groups[envelope.chatId];
    const wildcardGroupCfg = this.config.groups["*"];
    const configured = groupCfg?.groupHistoryLimit ?? wildcardGroupCfg?.groupHistoryLimit ?? this.config.groupHistoryLimit ?? 0;
    if (!Number.isFinite(configured) || configured <= 0) {
      return 0;
    }
    return Math.floor(configured);
  }
  recordPendingGroupHistory(envelope) {
    const limit = this.groupHistoryLimit(envelope);
    if (limit <= 0 || envelope.text.trim().length === 0) {
      return;
    }
    const senderId = truncateGroupHistoryField(envelope.senderId);
    if (!this.gate.isAllowed(senderId)) {
      return;
    }
    const entry = {
      senderId,
      senderName: truncateGroupHistoryField(envelope.senderName),
      text: envelope.text.slice(0, GROUP_HISTORY_ENTRY_TEXT_LIMIT),
      messageId: envelope.messageId === void 0 ? void 0 : truncateGroupHistoryField(envelope.messageId),
      timestamp: Date.now()
    };
    try {
      this.groupHistory.record(this.groupHistoryKey(envelope), entry, limit);
    } catch (err) {
      process.stderr.write(`[${this.name}] failed to record group history for chat ${sanitizeLogText(envelope.chatId, 64)}: ${err instanceof Error ? err.message : err}
`);
    }
  }
  drainPendingGroupHistory(envelope) {
    const limit = this.groupHistoryLimit(envelope);
    if (limit <= 0) {
      return [];
    }
    try {
      return this.groupHistory.drain(this.groupHistoryKey(envelope), limit);
    } catch (err) {
      process.stderr.write(`[${this.name}] failed to drain group history for chat ${sanitizeLogText(envelope.chatId, 64)}: ${err instanceof Error ? err.message : err}
`);
      return [];
    }
  }
  clearPendingGroupHistory(envelope) {
    if (!envelope.isGroup && this.config.sessionScope !== "single") {
      return;
    }
    try {
      if (this.config.sessionScope === "single") {
        this.groupHistory.clearAll();
      } else {
        this.groupHistory.clear(this.groupHistoryKey(envelope));
      }
    } catch (err) {
      process.stderr.write(`[${this.name}] failed to clear group history for chat ${sanitizeLogText(envelope.chatId, 64)}: ${err instanceof Error ? err.message : err}
`);
    }
  }
  prependGroupHistoryContext(promptText, entries) {
    if (entries.length === 0) {
      return promptText;
    }
    const lines = entries.filter((entry) => this.gate.isAllowed(entry.senderId));
    if (lines.length === 0) {
      return promptText;
    }
    const formatted = lines.map((entry) => {
      const who = sanitizeSenderName(entry.senderName || entry.senderId);
      const text = sanitizeQuotedText(entry.text, GROUP_HISTORY_ENTRY_TEXT_LIMIT);
      return `- [${who}] ${text}`;
    });
    return `${GROUP_HISTORY_CONTEXT_MARKER}
${formatted.join("\n")}

${CURRENT_MESSAGE_MARKER}
${promptText}`;
  }
  async handleInbound(envelope) {
    const groupResult = this.groupGate.check(envelope);
    if (!groupResult.allowed) {
      if (groupResult.reason === "mention_required") {
        this.recordPendingGroupHistory(envelope);
      }
      return;
    }
    const result = this.gate.check(envelope.senderId, envelope.senderName);
    if (!result.allowed) {
      if (result.pairingCode !== void 0) {
        await this.onPairingRequired(envelope.chatId, result.pairingCode);
      }
      return;
    }
    const parsed = this.parseCommand(envelope.text);
    if (parsed) {
      const handler = this.commands.get(parsed.command);
      if (handler) {
        const handled = await handler(envelope, parsed.args);
        if (handled)
          return;
      }
    }
    const bangText = envelope.text.trimStart();
    if (bangText.startsWith("!")) {
      if (envelope.isGroup || this.isSharedSession(envelope)) {
        const who = sanitizeSenderName(envelope.senderName || envelope.senderId || "unknown");
        process.stderr.write(`[${this.name}] blocked ! shell command from ${who} (sender ${envelope.senderId}) in chat ${sanitizeLogText(envelope.chatId, 64)}
`);
      }
      if (envelope.isGroup) {
        await this.sendMessage(envelope.chatId, "Shell commands (`!`) are disabled in group chats.");
        return;
      }
      if (this.isSharedSession(envelope)) {
        await this.sendMessage(envelope.chatId, "Shell commands (`!`) are disabled in shared sessions.");
        return;
      }
    }
    const sessionId = await this.router.resolve(this.name, envelope.senderId, envelope.chatId, envelope.threadId, this.config.cwd);
    if (bangText.startsWith("!")) {
      const cmd = bangText.slice(1).trim();
      const bridgeShellCommand = this.bridge.shellCommand;
      if (cmd && bridgeShellCommand) {
        try {
          const result2 = await bridgeShellCommand(sessionId, cmd);
          const longestRun = Math.max(0, ...Array.from((result2.output || "").matchAll(/`+/g), (m) => m[0].length));
          const fence = "`".repeat(Math.max(3, longestRun + 1));
          const output = result2.output ? `${fence}
${result2.output}
${fence}` : "(no output)";
          const exitLine = result2.exitCode !== null && result2.exitCode !== 0 ? `
Exit code: ${result2.exitCode}` : "";
          await this.sendMessage(envelope.chatId, `$ ${cmd}
${output}${exitLine}`);
        } catch (error) {
          await this.sendMessage(envelope.chatId, `Shell command failed: ${error instanceof Error ? error.message : String(error)}`);
        }
        return;
      }
    }
    const recognizedSlashCommand = this.isSlashCommand(envelope.text) && this.isRecognizedCommand(envelope.text, sessionId);
    let promptText = envelope.text;
    if ((envelope.isGroup || this.config.sessionScope === "single") && !envelope.alreadyPrefixed && !recognizedSlashCommand) {
      const who = sanitizeSenderName(envelope.senderName || envelope.senderId || "unknown");
      promptText = `[${who}] ${sanitizePromptText(promptText)}`;
    }
    if (envelope.referencedText) {
      const quoted = sanitizeQuotedText(envelope.referencedText, 500);
      promptText = `[Replying to: "${quoted}"]

${promptText}`;
    }
    let imageBase64 = envelope.imageBase64;
    let imageMimeType = envelope.imageMimeType;
    if (envelope.attachments?.length) {
      const filePaths = [];
      for (const att of envelope.attachments) {
        if (att.type === "image" && att.data && !imageBase64) {
          imageBase64 = att.data;
          imageMimeType = att.mimeType;
        } else if (att.filePath) {
          const label = att.type === "file" ? "file" : att.type;
          const name = att.fileName ? ` "${sanitizeQuotedText(att.fileName, 128)}"` : "";
          const renderedPath = sanitizePromptPath(att.filePath);
          filePaths.push(`User sent a ${label}${name}. It has been saved to: ${renderedPath}`);
        }
      }
      if (filePaths.length > 0) {
        promptText = promptText + "\n\n" + filePaths.join("\n");
      }
    }
    const groupCfg = envelope.isGroup ? this.config.groups[envelope.chatId] || this.config.groups["*"] : void 0;
    const mode = groupCfg?.dispatchMode || this.config.dispatchMode || "steer";
    const active = this.activePrompts.get(sessionId);
    let steerWatchdog;
    if (active) {
      switch (mode) {
        case "collect": {
          let buffer = this.collectBuffers.get(sessionId);
          if (!buffer) {
            buffer = [];
            this.collectBuffers.set(sessionId, buffer);
          }
          buffer.push({ text: promptText, envelope });
          return;
        }
        case "steer": {
          if (!this.isAuthorizedForSharedSession(envelope)) {
            process.stderr.write(`[${this.name}] steer denied for ${envelope.senderId} in shared session (chat=${sanitizeLogText(envelope.chatId, 64)}); queuing instead
`);
            break;
          }
          active.cancelled = true;
          process.stderr.write(`[${this.name}] steer: cancelled active turn for ${envelope.senderId} in session ${sessionId}
`);
          this.stopActiveStreaming(active, sessionId, "steer");
          void this.bridge.cancelSession(sessionId).catch((err) => {
            process.stderr.write(`[${this.name}] cancelSession failed for session=${sessionId} (steer): ${err instanceof Error ? err.message : err}
`);
          });
          steerWatchdog = setTimeout(() => {
            if (this.activePrompts.get(sessionId) === active) {
              process.stderr.write(`[${this.name}] steer queued behind active turn for session ${sessionId}: still waiting after ${CLEAR_CANCEL_TIMEOUT_MS}ms (use /clear to recover)
`);
            }
          }, CLEAR_CANCEL_TIMEOUT_MS);
          steerWatchdog.unref?.();
          promptText = `[The user sent a new message while you were working. Their previous request has been cancelled.]

${promptText}`;
          break;
        }
        case "followup": {
          break;
        }
        default: {
          const _exhaustive = mode;
          throw new Error(`Unknown dispatch mode: ${_exhaustive}`);
        }
      }
    }
    let shouldPrependSessionContext = !this.instructedSessions.has(sessionId);
    if (shouldPrependSessionContext) {
      this.instructedSessions.add(sessionId);
    }
    const prev = this.sessionQueues.get(sessionId) ?? Promise.resolve();
    const generation = this.sessionGenerations.get(sessionId) ?? 0;
    const useBlockStreaming = this.config.blockStreaming === "on";
    const current = prev.then(async () => {
      clearTimeout(steerWatchdog);
      if (this.dropQueuedTurnIfStale(sessionId, generation, envelope)) {
        return;
      }
      if (!shouldPrependSessionContext && !this.instructedSessions.has(sessionId)) {
        shouldPrependSessionContext = true;
        this.instructedSessions.add(sessionId);
      }
      const sessionContext = [];
      if (shouldPrependSessionContext) {
        let memoryText;
        if (this.channelMemory && this.isAuthorizedForChannelMemory(envelope) && (!this.isSharedSession(envelope) || this.config.senderPolicy === "allowlist")) {
          try {
            memoryText = (await this.channelMemory.readChannelMemory(this.channelMemoryTarget(envelope)))?.trim();
          } catch (error) {
            this.logChannelMemoryError("read", envelope, this.channelMemoryErrorMessage(error));
            this.instructedSessions.delete(sessionId);
          }
        }
        if (memoryText) {
          sessionContext.push(`Channel memory for this chat:
${sanitizePromptText(memoryText)}`);
        }
        if (this.config.instructions) {
          sessionContext.push(this.config.instructions);
        }
      }
      if (this.dropQueuedTurnIfStale(sessionId, generation, envelope)) {
        return;
      }
      const groupHistoryEntries = recognizedSlashCommand ? [] : this.drainPendingGroupHistory(envelope);
      let promptToSend = this.prependGroupHistoryContext(promptText, groupHistoryEntries);
      if (sessionContext.length > 0) {
        promptToSend = `${sessionContext.join("\n\n")}

${promptToSend}`;
      }
      let doneResolve = /* @__PURE__ */ __name(() => {
      }, "doneResolve");
      const done = new Promise((r) => {
        doneResolve = r;
      });
      const promptState = {
        cancelled: false,
        done,
        resolve: doneResolve,
        chatId: envelope.chatId,
        messageId: envelope.messageId
      };
      this.activePrompts.set(sessionId, promptState);
      this.onPromptStart(envelope.chatId, sessionId, envelope.messageId);
      const streamer = useBlockStreaming ? new BlockStreamer({
        minChars: this.config.blockStreamingChunk?.minChars ?? 400,
        maxChars: this.config.blockStreamingChunk?.maxChars ?? 1e3,
        idleMs: this.config.blockStreamingCoalesce?.idleMs ?? 1500,
        send: /* @__PURE__ */ __name((text) => this.sendMessage(envelope.chatId, text), "send")
      }) : null;
      promptState.stopStreaming = () => streamer?.stop();
      const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
        if (sid === sessionId && !promptState.cancelled) {
          this.onResponseChunk(envelope.chatId, chunk, sessionId);
          streamer?.push(chunk);
        }
      }, "onChunk");
      const promptBridge = this.bridge;
      promptBridge.on("textChunk", onChunk);
      try {
        const response = await promptBridge.prompt(sessionId, promptToSend, {
          imageBase64,
          imageMimeType
        });
        if (promptState.cancelRequested && !promptState.cancelled) {
          const cancelled = await promptState.cancelRequested;
          if (cancelled) {
            promptState.cancelled = true;
          }
        }
        if (!promptState.cancelled && response) {
          if (streamer) {
            await streamer.flush();
          } else {
            await this.onResponseComplete(envelope.chatId, response, sessionId);
          }
        }
      } finally {
        promptBridge.off("textChunk", onChunk);
        streamer?.stop();
        const stillCurrent = this.activePrompts.get(sessionId) === promptState;
        if (!promptState.clearEvicted) {
          try {
            this.onPromptEnd(envelope.chatId, sessionId, envelope.messageId);
          } catch (err) {
            process.stderr.write(`[${this.name}] onPromptEnd threw in finally for session ${sessionId}: ${err instanceof Error ? err.message : err}
`);
          }
        }
        if (stillCurrent) {
          this.activePrompts.delete(sessionId);
        }
        promptState.resolve();
        const buffer = this.collectBuffers.get(sessionId);
        if (stillCurrent && buffer && buffer.length > 0) {
          this.collectBuffers.delete(sessionId);
          const lost = buffer.length;
          const coalesced = buffer.map((b) => b.text).join("\n\n");
          const lastEnvelope = buffer[buffer.length - 1].envelope;
          const syntheticEnvelope = {
            ...lastEnvelope,
            text: coalesced,
            // Coalesced text already carries each message's [sender] prefix.
            alreadyPrefixed: true,
            // Clear attachments/references — already resolved in original text
            referencedText: void 0,
            attachments: void 0,
            imageBase64: void 0,
            imageMimeType: void 0
          };
          this.handleInbound(syntheticEnvelope).catch((err) => {
            process.stderr.write(`[${this.name}] dropped ${lost} buffered message(s) on collect re-entry for session ${sessionId} (last sender ${lastEnvelope.senderId}): ${err instanceof Error ? err.message : String(err)}
`);
          });
        }
      }
    });
    this.sessionQueues.set(sessionId, current.catch(() => {
    }));
    await current;
  }
  async onPairingRequired(chatId, code) {
    if (code) {
      await this.sendMessage(chatId, `Your pairing code is: ${code}

Ask the bot operator to approve you with:
  qwen channel pairing approve ${this.name} ${code}`);
    } else {
      await this.sendMessage(chatId, "Too many pending pairing requests. Please try again later.");
    }
  }
};
function truncateGroupHistoryField(value) {
  return value.slice(0, GROUP_HISTORY_ENTRY_METADATA_LIMIT);
}
__name(truncateGroupHistoryField, "truncateGroupHistoryField");
function truncateLoopLabel(prompt) {
  const chars = Array.from(prompt);
  return chars.length > 60 ? `${chars.slice(0, 57).join("")}...` : prompt;
}
__name(truncateLoopLabel, "truncateLoopLabel");

// packages/channels/base/dist/ChannelLoopStore.js
init_esbuild_shims();
import * as crypto2 from "node:crypto";
import * as fs2 from "node:fs/promises";
import * as path3 from "node:path";
var ChannelLoopStore = class {
  static {
    __name(this, "ChannelLoopStore");
  }
  filePath;
  now;
  idFactory;
  pendingUpdate = Promise.resolve();
  constructor(options) {
    this.filePath = options.filePath;
    this.now = options.now ?? (() => /* @__PURE__ */ new Date());
    this.idFactory = options.idFactory ?? (() => crypto2.randomUUID());
  }
  async list() {
    return this.readJobs();
  }
  async listForTarget(channelName, target) {
    const jobs = await this.readJobs();
    return jobs.filter((job) => job.channelName === channelName && sameTarget(job.target, target));
  }
  async create(input) {
    let job;
    await this.updateJobs((jobs) => {
      job = this.buildLoop(input, jobs);
      return [...jobs, job];
    });
    if (!job)
      throw new Error("Failed to create channel loop.");
    return job;
  }
  async createForTarget(input, maxEnabledLoops) {
    let created;
    await this.updateJobs((jobs) => {
      const enabledForTarget = jobs.filter((job2) => job2.enabled && job2.channelName === input.channelName && sameTarget(job2.target, input.target)).length;
      if (enabledForTarget >= maxEnabledLoops) {
        return jobs;
      }
      const job = this.buildLoop(input, jobs);
      created = job;
      return [...jobs, job];
    });
    return created;
  }
  async update(id, patch) {
    let found = false;
    await this.updateJobs((jobs) => jobs.map((job) => {
      if (job.id !== id)
        return job;
      found = true;
      return { ...job, ...patch };
    }));
    return found;
  }
  async disable(id) {
    return this.update(id, { enabled: false });
  }
  buildLoop(input, existingLoops) {
    const existingIds = new Set(existingLoops.map((loop) => loop.id));
    const baseId = this.idFactory();
    let id = baseId;
    let suffix = 1;
    while (existingIds.has(id)) {
      id = `${baseId}-${suffix++}`;
    }
    return {
      ...input,
      id,
      target: normalizeTarget(input.target),
      enabled: true,
      createdAt: this.now().toISOString(),
      consecutiveFailures: 0,
      runCount: 0
    };
  }
  async updateJobs(mutate) {
    const nextUpdate = this.pendingUpdate.then(async () => {
      const jobs = await this.readJobs();
      await this.writeJobs(mutate(jobs));
    });
    this.pendingUpdate = nextUpdate.catch(() => {
    });
    await nextUpdate;
  }
  async readJobs() {
    let raw;
    try {
      raw = await fs2.readFile(this.filePath, "utf8");
    } catch (err) {
      if (err.code === "ENOENT")
        return [];
      throw err;
    }
    let parsed;
    try {
      parsed = JSON.parse(raw);
    } catch {
      throw new Error(`Malformed JSON in ${this.filePath}; fix or delete the file.`);
    }
    if (!Array.isArray(parsed)) {
      throw new Error(`Expected a JSON array in ${this.filePath}; fix or delete the file.`);
    }
    const jobs = [];
    for (const [index, value] of parsed.entries()) {
      if (!isChannelLoop(value)) {
        process.stderr.write(`Invalid channel loop at index ${index} in ${this.filePath}: ${JSON.stringify(value)}
`);
        continue;
      }
      jobs.push(normalizeJob(value));
    }
    return jobs;
  }
  async writeJobs(jobs) {
    const dir = path3.dirname(this.filePath);
    await fs2.mkdir(dir, { recursive: true, mode: 448 });
    await fs2.chmod(dir, 448).catch(() => {
    });
    const tmpPath = `${this.filePath}.${crypto2.randomBytes(6).toString("hex")}.tmp`;
    try {
      await fs2.writeFile(tmpPath, JSON.stringify(jobs, null, 2), {
        encoding: "utf8",
        mode: 384
      });
      await fs2.rename(tmpPath, this.filePath);
      await fs2.chmod(this.filePath, 384).catch(() => {
      });
    } catch (err) {
      await fs2.rm(tmpPath, { force: true }).catch(() => {
      });
      throw err;
    }
  }
};
function sameTarget(a, b) {
  return a.channelName === b.channelName && a.senderId === b.senderId && a.chatId === b.chatId && a.threadId === b.threadId && a.isGroup === b.isGroup;
}
__name(sameTarget, "sameTarget");
function normalizeTarget(target) {
  return {
    ...target,
    isGroup: target.isGroup === void 0 ? void 0 : target.isGroup === true
  };
}
__name(normalizeTarget, "normalizeTarget");
function isSessionTarget(value) {
  if (typeof value !== "object" || value === null)
    return false;
  const target = value;
  return typeof target["channelName"] === "string" && typeof target["senderId"] === "string" && typeof target["chatId"] === "string" && (target["threadId"] === void 0 || typeof target["threadId"] === "string") && (target["isGroup"] === void 0 || typeof target["isGroup"] === "boolean");
}
__name(isSessionTarget, "isSessionTarget");
function isChannelLoop(value) {
  if (typeof value !== "object" || value === null)
    return false;
  const job = value;
  return typeof job["id"] === "string" && typeof job["channelName"] === "string" && isSessionTarget(job["target"]) && typeof job["cwd"] === "string" && typeof job["cron"] === "string" && typeof job["prompt"] === "string" && (job["label"] === void 0 || typeof job["label"] === "string") && typeof job["recurring"] === "boolean" && typeof job["enabled"] === "boolean" && typeof job["createdBy"] === "string" && typeof job["createdAt"] === "string" && (job["lastFiredAt"] === void 0 || typeof job["lastFiredAt"] === "string") && (job["lastFinishedAt"] === void 0 || typeof job["lastFinishedAt"] === "string") && (job["lastResultPreview"] === void 0 || typeof job["lastResultPreview"] === "string") && (job["lastStatus"] === void 0 || job["lastStatus"] === "ok" || job["lastStatus"] === "error") && (job["lastError"] === void 0 || typeof job["lastError"] === "string") && typeof job["consecutiveFailures"] === "number" && (job["runningSince"] === void 0 || typeof job["runningSince"] === "string") && (job["runCount"] === void 0 || typeof job["runCount"] === "number");
}
__name(isChannelLoop, "isChannelLoop");
function normalizeJob(job) {
  return {
    ...job,
    target: normalizeTarget(job.target),
    runCount: job.runCount ?? 0
  };
}
__name(normalizeJob, "normalizeJob");

export {
  resolvePath,
  getGlobalQwenDir,
  AcpBridge,
  DaemonChannelBridge,
  PairingStore,
  sanitizeSenderName,
  sanitizePromptText,
  sanitizeLogText,
  SessionRouter,
  ChannelLoopScheduler,
  ChannelBase,
  ChannelLoopStore
};
