// Force strict mode and setup for ESM
"use strict";
import {
  ClientSideConnection,
  PROTOCOL_VERSION,
  ndJsonStream
} from "./chunk-VRDMOSQS.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 = new RegExp("[\\u0080-\\u009f\\p{Cf}\\u2028\\u2029]|\\p{Variation_Selector}", "gu");
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");
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();
  turnBarriers = /* @__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) ?? [];
  }
  listSessions() {
    const result = [];
    for (const session of this.sessions.values()) {
      result.push({
        sessionId: session.sessionId,
        workspaceCwd: session.workspaceCwd,
        hasActivePrompt: this.activePrompts.has(session.sessionId)
      });
    }
    return result;
  }
  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 turnBarrier = this.createTurnBarrier(sessionId);
    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 Promise.race([
        turnBarrier,
        new Promise((resolve2) => setTimeout(resolve2, 0))
      ]);
      const textResult = chunks.join("");
      this.emit("promptComplete", {
        sessionId,
        text: textResult,
        stopReason: result.stopReason
      });
      return textResult;
    } finally {
      this.clearTurnBarrier(sessionId);
      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);
    this.resolveTurnBarrier(sessionId);
    this.abortActivePrompts(sessionId);
    this.activePrompts.delete(sessionId);
    await session.cancel();
  }
  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;
      case "turn_complete":
        this.resolveTurnBarrier(session.sessionId);
        break;
      case "turn_error":
        this.emitProtocolError(`Daemon turn error for session ${session.sessionId}`, event.data);
        this.resolveTurnBarrier(session.sessionId);
        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.resolveTurnBarrier(sessionId);
    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);
  }
  createTurnBarrier(sessionId) {
    return new Promise((resolve2) => {
      this.turnBarriers.set(sessionId, resolve2);
    });
  }
  resolveTurnBarrier(sessionId) {
    const resolve2 = this.turnBarriers.get(sessionId);
    if (resolve2) {
      this.turnBarriers.delete(sessionId);
      resolve2();
    }
  }
  clearTurnBarrier(sessionId) {
    this.turnBarriers.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");
  }
  reason;
  constructor(message, reason = "dropped") {
    super(message);
    this.reason = reason;
  }
};
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 LOOP_TIMED_OUT_MESSAGE = "loop timed out";
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 (defaulted + frozen) identity/scope — adapters should read these, not raw config. */
  identity;
  memoryScope;
  /** 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) => {
    this.dispatchToolCall(event);
  }, "bridgeToolCallListener");
  bridgeSessionDiedListener = /* @__PURE__ */ __name((event) => {
    this.onSessionDied(event.sessionId);
  }, "bridgeSessionDiedListener");
  dispatchToolCall(event) {
    const target = this.router.getTarget(event.sessionId);
    const active = this.activePrompts.get(event.sessionId);
    const chatId = active?.chatId ?? target?.chatId;
    if (!chatId) {
      return;
    }
    if (active && !active.cancelled && !active.cancelPending) {
      const safeToolCall = {
        sessionId: event.sessionId,
        toolCallId: event.toolCallId,
        kind: sanitizeLogText(event.kind ?? "", 20),
        title: sanitizeLogText(event.title ?? "", 80),
        status: sanitizeLogText(event.status ?? "", 20)
      };
      this.emitTaskLifecycle({
        ...this.lifecycleBase(chatId, event.sessionId, active.messageId),
        type: "tool_call",
        toolCall: safeToolCall
      });
    }
    this.onToolCall(chatId, event);
  }
  constructor(name, config, bridge, options) {
    this.name = name;
    this.config = config;
    this.bridge = bridge;
    this.proxy = options?.proxy;
    this.identity = Object.freeze(this.resolveIdentity(name, config));
    this.memoryScope = Object.freeze(this.resolveMemoryScope(name, config));
    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);
    }
  }
  /**
   * Adapter hook for task lifecycle events — the canonical way to track task
   * state (onPromptStart/onPromptEnd are retained for back-compat). The prompt
   * flow never awaits this hook; an async override's rejection is caught and
   * logged, nothing more.
   */
  onTaskLifecycle(_event) {
  }
  emitTaskLifecycle(event) {
    try {
      const result = this.onTaskLifecycle(event);
      if (result && typeof result.catch === "function") {
        result.catch((err) => {
          this.logTaskLifecycleError(event, err);
        });
      }
    } catch (err) {
      this.logTaskLifecycleError(event, err);
    }
  }
  logTaskLifecycleError(event, err) {
    const channel = sanitizeLogText(this.name, 64);
    const sessionId = sanitizeLogText(event.sessionId, 64);
    const stack = err instanceof Error && err.stack ? ` | ${sanitizeLogText(err.stack, 500)}` : "";
    process.stderr.write(`[${channel}] onTaskLifecycle threw for ${event.type} session ${sessionId}: ${this.lifecycleError(err)}${stack}
`);
  }
  lifecycleError(err) {
    return sanitizeLogText(err instanceof Error ? err.message : String(err), 200);
  }
  emitTaskCancellation(active, sessionId, reason) {
    if (active.cancellationEmitted) {
      return;
    }
    active.cancellationEmitted = true;
    this.emitTaskLifecycle({
      ...this.lifecycleBase(active.chatId, sessionId, active.messageId),
      type: "cancelled",
      reason
    });
  }
  resolveIdentity(name, config) {
    return {
      id: config.identity?.id || `channel:${name}`,
      displayName: config.identity?.displayName || name,
      ...config.identity?.description ? { description: config.identity.description } : {}
    };
  }
  resolveMemoryScope(name, config) {
    return {
      namespace: config.memoryScope?.namespace || `channel:${name}`,
      mode: config.memoryScope?.mode ?? "metadata-only"
    };
  }
  /** Built once — identity/memoryScope are frozen at construction. */
  boundaryPrompt;
  channelBoundaryPrompt() {
    if (this.boundaryPrompt !== void 0) {
      return this.boundaryPrompt;
    }
    const identityLines = [
      "Channel identity:",
      `- id: ${sanitizeQuotedText(this.identity.id, 128)}`,
      `- display name: ${sanitizeQuotedText(this.identity.displayName, 128)}`,
      ...this.identity.description ? [
        `- description: ${sanitizeQuotedText(this.identity.description, 256)}`
      ] : []
    ];
    const memoryLines = [
      "Memory scope:",
      `- namespace: ${sanitizeQuotedText(this.memoryScope.namespace, 128)}`,
      `- mode: ${this.memoryScope.mode}`,
      "- data from other channels must not be shared."
    ];
    this.boundaryPrompt = [...identityLines, "", ...memoryLines].join("\n");
    return this.boundaryPrompt;
  }
  shouldPrependChannelBoundaryPrompt() {
    return Boolean(this.config.identity || this.config.memoryScope);
  }
  lifecycleBase(chatId, sessionId, messageId) {
    return {
      channelName: this.name,
      chatId,
      sessionId,
      ...messageId ? { messageId } : {},
      identity: this.identity,
      memoryScope: this.memoryScope
    };
  }
  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}] Scheduled task running unattended: no one is present to answer questions, and your final response is delivered to this chat automatically \u2014 do whatever work the task requires, then put the result in your final response instead of trying to deliver it to this chat yourself.

${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 (this.shouldPrependChannelBoundaryPrompt()) {
          context.push(this.channelBoundaryPrompt());
        }
        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,
        loopPrompt: true
      };
      this.activePrompts.set(sessionId, promptState);
      this.emitTaskLifecycle({
        ...this.lifecycleBase(job.target.chatId, sessionId, job.id),
        type: "started"
      });
      try {
        this.onPromptStart(job.target.chatId, sessionId);
      } catch (err) {
        process.stderr.write(`[${this.name}] onPromptStart threw in loop ${job.id} for session ${sessionId}: ${this.lifecycleError(err)}
`);
      }
      const heldChunks = [];
      const releaseHeldChunks = /* @__PURE__ */ __name(() => {
        for (const held of heldChunks.splice(0)) {
          this.emitTaskLifecycle({
            ...this.lifecycleBase(job.target.chatId, sessionId, job.id),
            type: "text_chunk",
            chunk: held
          });
          this.onResponseChunk(job.target.chatId, held, sessionId);
        }
      }, "releaseHeldChunks");
      const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
        if (sid !== sessionId || promptState.cancelled) {
          return;
        }
        heldChunks.push(chunk);
        if (!promptState.cancelPending) {
          releaseHeldChunks();
        }
      }, "onChunk");
      const promptBridge = this.bridge;
      promptBridge.on("textChunk", onChunk);
      try {
        const response = await this.runLoopBridgePrompt(promptBridge, sessionId, promptText, promptState, job.id, options.timeoutMs);
        await this.settleCancelRequested(promptState);
        if (promptState.cancelled) {
          throw new ChannelLoopSkippedError("loop cancelled before delivery", "cancel_command");
        }
        releaseHeldChunks();
        if (options.shouldContinue && !await options.shouldContinue()) {
          throw new ChannelLoopSkippedError("loop dropped before delivery");
        }
        if (promptState.cancelled) {
          throw new ChannelLoopSkippedError("loop cancelled before delivery", "cancel_command");
        }
        if (response) {
          promptState.deliveryStarted = true;
          await this.pushProactive(job.target, response);
        }
        if (!promptState.deliveryStarted) {
          await this.settleCancelRequested(promptState);
          if (promptState.cancelled) {
            throw new ChannelLoopSkippedError("loop cancelled before delivery", "cancel_command");
          }
        }
        if (!promptState.cancellationEmitted) {
          this.emitTaskLifecycle({
            ...this.lifecycleBase(job.target.chatId, sessionId, job.id),
            type: "completed"
          });
        }
        return response;
      } catch (err) {
        if (!promptState.deliveryStarted) {
          await this.settleCancelRequested(promptState);
        }
        if (err instanceof ChannelLoopSkippedError && !promptState.cancelled) {
          this.emitTaskCancellation(promptState, sessionId, err.reason);
          promptState.cancelled = true;
        }
        if (!promptState.cancelled && !(err instanceof ChannelLoopSkippedError)) {
          releaseHeldChunks();
          this.emitTaskLifecycle({
            ...this.lifecycleBase(job.target.chatId, sessionId, job.id),
            type: "failed",
            error: this.lifecycleError(err),
            phase: promptState.deliveryStarted ? "delivery" : "agent"
          });
        } else if (promptState.cancelled && !(err instanceof ChannelLoopSkippedError) && !(err instanceof Error && err.message === LOOP_TIMED_OUT_MESSAGE)) {
          const channel = sanitizeLogText(this.name, 64);
          const safeJobId = sanitizeLogText(job.id, 64);
          const safeSessionId = sanitizeLogText(sessionId, 64);
          process.stderr.write(`[${channel}] loop ${safeJobId} threw after cancellation for session ${safeSessionId}: ${this.lifecycleError(err)}
`);
        }
        throw err;
      } 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_MESSAGE));
          }, timeoutMs);
          timer.unref?.();
        })
      ]);
    } catch (err) {
      if (err instanceof Error && err.message === LOOP_TIMED_OUT_MESSAGE) {
        promptState.cancelled = true;
        await this.cancelTimedOutLoopPrompt(promptBridge, sessionId, jobId);
        this.emitTaskCancellation(promptState, sessionId, "timeout");
      }
      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);
    }
  }
  requestActivePromptCancellation(sessionId, reason = "cancel_command") {
    const active = this.activePrompts.get(sessionId);
    if (!active) {
      return this.bridge.cancelSession(sessionId).then(() => true, (err) => {
        this.logCancelSessionFailure(sessionId, err);
        return false;
      });
    }
    if (active.deliveryStarted) {
      return Promise.resolve(false);
    }
    const cancelRequested = active.cancelRequested ?? this.bridge.cancelSession(sessionId).then(() => true, (err) => {
      this.logCancelSessionFailure(sessionId, err);
      active.cancelRequested = void 0;
      return false;
    });
    active.cancelRequested = cancelRequested;
    active.cancelPending = true;
    return cancelRequested.finally(() => {
      active.cancelPending = false;
    }).then((cancelSucceeded) => {
      const turnEnded = this.activePrompts.get(sessionId) !== active;
      if (!cancelSucceeded || active.deliveryStarted || turnEnded && !active.cancelled) {
        return false;
      }
      active.cancelled = true;
      this.stopActiveStreaming(active, sessionId, reason);
      this.collectBuffers.delete(sessionId);
      this.emitTaskCancellation(active, sessionId, reason);
      return true;
    });
  }
  logCancelSessionFailure(sessionId, err) {
    process.stderr.write(`[${sanitizeLogText(this.name, 64)}] cancelSession failed for session=${sanitizeLogText(sessionId, 64)}: ${this.lifecycleError(err)}
`);
  }
  async settleCancelRequested(active) {
    if (!active.cancelRequested || active.cancelled) {
      return;
    }
    let timer;
    try {
      const cancelled = await Promise.race([
        active.cancelRequested,
        new Promise((resolve2) => {
          timer = setTimeout(() => resolve2(false), CLEAR_CANCEL_TIMEOUT_MS);
          timer.unref?.();
        })
      ]);
      if (cancelled) {
        active.cancelled = true;
      }
    } finally {
      clearTimeout(timer);
    }
  }
  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 cancelSucceeded = await this.requestActivePromptCancellation(activeSessionId, "cancel_command");
      await this.sendMessage(envelope.chatId, cancelSucceeded ? "Cancelled current request." : "Failed to cancel 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.loopPrompt ? void 0 : 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}`,
        // Identity/memory lines only for channels that opted in — keep
        // unconfigured channels' output unchanged.
        ...this.shouldPrependChannelBoundaryPrompt() ? [
          `Identity: ${sanitizeQuotedText(this.identity.displayName, 128)}`,
          `Memory: ${sanitizeQuotedText(this.memoryScope.namespace, 128)}`
        ] : [],
        // 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}`,
        ...this.shouldPrependChannelBoundaryPrompt() ? [
          `Identity: ${sanitizeQuotedText(this.identity.id, 128)}`,
          `Memory: ${this.memoryScope.mode}`
        ] : []
      ];
      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}
`);
    });
    this.emitTaskCancellation(active, sessionId, "clear");
    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;
          }
          const firstCancellation = !active.cancelled;
          active.cancelled = true;
          if (firstCancellation) {
            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}
`);
            });
            this.emitTaskCancellation(active, sessionId, "steer");
          }
          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.shouldPrependChannelBoundaryPrompt()) {
          sessionContext.push(this.channelBoundaryPrompt());
        }
      }
      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.emitTaskLifecycle({
        ...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
        type: "started"
      });
      try {
        this.onPromptStart(envelope.chatId, sessionId, envelope.messageId);
      } catch (err) {
        process.stderr.write(`[${this.name}] onPromptStart threw for session ${sessionId}: ${this.lifecycleError(err)}
`);
      }
      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 heldChunks = [];
      const releaseHeldChunks = /* @__PURE__ */ __name(() => {
        for (const held of heldChunks.splice(0)) {
          this.emitTaskLifecycle({
            ...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
            type: "text_chunk",
            chunk: held
          });
          this.onResponseChunk(envelope.chatId, held, sessionId);
          streamer?.push(held);
        }
      }, "releaseHeldChunks");
      const onChunk = /* @__PURE__ */ __name((sid, chunk) => {
        if (sid !== sessionId || promptState.cancelled) {
          return;
        }
        heldChunks.push(chunk);
        if (!promptState.cancelPending) {
          releaseHeldChunks();
        }
      }, "onChunk");
      const promptBridge = this.bridge;
      promptBridge.on("textChunk", onChunk);
      try {
        const response = await promptBridge.prompt(sessionId, promptToSend, {
          imageBase64,
          imageMimeType
        });
        await this.settleCancelRequested(promptState);
        if (!promptState.cancelled) {
          releaseHeldChunks();
        }
        if (!promptState.cancelled && response) {
          promptState.deliveryStarted = true;
          if (streamer) {
            await streamer.flush();
          } else {
            await this.onResponseComplete(envelope.chatId, response, sessionId);
          }
        }
        if (!promptState.deliveryStarted) {
          await this.settleCancelRequested(promptState);
        }
        if (!promptState.cancelled && !promptState.cancellationEmitted) {
          this.emitTaskLifecycle({
            ...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
            type: "completed"
          });
        }
      } catch (err) {
        if (!promptState.deliveryStarted) {
          await this.settleCancelRequested(promptState);
        }
        if (!promptState.cancelled) {
          releaseHeldChunks();
          this.emitTaskLifecycle({
            ...this.lifecycleBase(envelope.chatId, sessionId, envelope.messageId),
            type: "failed",
            error: this.lifecycleError(err),
            phase: promptState.deliveryStarted ? "delivery" : "agent"
          });
        } else {
          const channel = sanitizeLogText(this.name, 64);
          const safeSessionId = sanitizeLogText(sessionId, 64);
          const safeMessageId = sanitizeLogText(envelope.messageId ?? "", 64);
          process.stderr.write(`[${channel}] turn ${safeMessageId} threw after cancellation for session ${safeSessionId}: ${this.lifecycleError(err)}
`);
        }
        throw err;
      } 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");

// packages/channels/base/dist/types.js
init_esbuild_shims();
function isTerminalTaskLifecycleType(type) {
  return type === "completed" || type === "cancelled" || type === "failed";
}
__name(isTerminalTaskLifecycleType, "isTerminalTaskLifecycleType");

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