    sendTextReply: deps.sendTextReply,
  });
}

export function createTelegramCommandTargetRuntime<
  TMessage extends TelegramCommandRuntimeMessage,
  TContext,
>(
  deps: TelegramCommandTargetRuntimeDeps<TContext>,
): TelegramCommandTargetRuntime<TMessage, TContext> {
  return {
    enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
      deps.enqueueControlItem(
        getTelegramCommandMessageTarget(message),
        ctx,
        controlType,
        statusSummary,
        execute,
      );
    },
    showStatus: (message, ctx) => {
      const target = getTelegramCommandMessageTarget(message);
      return deps.showStatus(target.chatId, target.replyToMessageId, ctx);
    },
    openModelMenu: (message, ctx) => {
      const target = getTelegramCommandMessageTarget(message);
      return deps.openModelMenu(target.chatId, target.replyToMessageId, ctx);
    },
    openSettingsMenu: async (message, ctx) => {
      const target = getTelegramCommandMessageTarget(message);
      if (!deps.openSettingsMenu) {
        await deps.sendTextReply(
          target.chatId,
          target.replyToMessageId,
          "Settings menu is unavailable.",
        );
        return;
      }
      await deps.openSettingsMenu(target.chatId, target.replyToMessageId, ctx);
    },
    sendTextReply: async (message, text) => {
      const target = getTelegramCommandMessageTarget(message);
      await deps.sendTextReply(target.chatId, target.replyToMessageId, text);
    },
  };
}

export interface TelegramCommandOrPromptRuntimeDeps<TMessage, TContext> {
  extractRawText: (messages: TMessage[]) => string;
  handleCommand: (
    commandName: string | undefined,
    message: TMessage,
    ctx: TContext,
  ) => Promise<boolean>;
  expandPromptTemplateCommand?: (
    commandName: string,
    args: string,
  ) => string | undefined;
  replaceMessageText: (message: TMessage, text: string) => TMessage;
  enqueueTurn: (messages: TMessage[], ctx: TContext) => Promise<void>;
}

export interface TelegramCommandRuntimeDeps<
  TMessage extends TelegramCommandRuntimeMessage,
  TContext,
> extends TelegramRuntimeEventRecorderPort {
  hasAbortHandler: () => boolean;
  clearPendingModelSwitch: () => void;
  hasQueuedTelegramItems: () => boolean;
  clearQueuedTelegramItems: (ctx: TContext) => number;
  setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
  abortCurrentTurn: () => void;
  isIdle: (ctx: TContext) => boolean;
  hasPendingMessages: (ctx: TContext) => boolean;
  hasActiveTelegramTurn: () => boolean;
  hasDispatchPending: () => boolean;
  isCompactionInProgress: () => boolean;
  setCompactionInProgress: (inProgress: boolean) => void;
  updateStatus: (ctx: TContext) => void;
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
  enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
  compact: (
    ctx: TContext,
    callbacks: { onComplete: () => void; onError: (error: unknown) => void },
  ) => void;
  enqueueControlItem: (
    message: TMessage,
    ctx: TContext,
    controlType: TelegramControlCommandType,
    statusSummary: string,
    execute: (ctx: TContext) => Promise<void>,
  ) => void;
  showStatus: (message: TMessage, ctx: TContext) => Promise<void>;
  openModelMenu: (message: TMessage, ctx: TContext) => Promise<void>;
  openThinkingMenu: (message: TMessage, ctx: TContext) => Promise<void>;
  openQueueMenu: (message: TMessage, ctx: TContext) => Promise<void>;
  openSettingsMenu?: (message: TMessage, ctx: TContext) => Promise<void>;
  getAllowedUserId: () => number | undefined;
  setAllowedUserId: (userId: number) => void;
  registerBotCommands: () => Promise<void>;
  getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
  persistConfig: () => Promise<void>;
  sendTextReply: (message: TMessage, text: string) => Promise<void>;
  sendUserMessage?: (
    message: string,
    options?: { deliverAs?: "steer" | "followUp" },
  ) => void;
}

export const TELEGRAM_APP_MENU_INTRO_HTML = [
  "<b>π Telegram bridge</b>",
  "",
  `${formatTelegramCommandEmojiPrefix("start")}/start — Open menu / Pair bridge`,
  `${formatTelegramCommandEmojiPrefix("compact")}/compact — Compact current session`,
  `${formatTelegramCommandEmojiPrefix("new")}/new — Start new clean session`,
  `${formatTelegramCommandEmojiPrefix("next")}/next — Force next turn`,
  `${formatTelegramCommandEmojiPrefix("continue")}/continue — Queue continue prompt`,
  `${formatTelegramCommandEmojiPrefix("abort")}/abort — Abort π`,
  `${formatTelegramCommandEmojiPrefix("stop")}/stop — Abort π & Clear queue`,
].join("\n");

function escapeTelegramCommandMenuHtml(text: string): string {
  return text
    .replace(/&/g, "&")
    .replace(/</g, "<")
    .replace(/>/g, ">");
}

function buildTelegramPromptTemplateMenuHtml(
  promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
): string {
  if (promptTemplates.length === 0) return "";
  return promptTemplates
    .map((template) => `🧩 /${escapeTelegramCommandMenuHtml(template.command)}`)
    .join("\n");
}

export function buildTelegramAppMenuHtml(
  statusHtml: string,
  promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [],
): string {
  const promptTemplateHtml =
    buildTelegramPromptTemplateMenuHtml(promptTemplates);
  if (!promptTemplateHtml)
    return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${statusHtml}`;
  return `${TELEGRAM_APP_MENU_INTRO_HTML}\n\n${promptTemplateHtml}\n\n${statusHtml}`;
}

export function createTelegramAppMenuHtmlBuilder<TContext>(deps: {
  buildStatusHtml: (ctx: TContext) => string;
  getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[];
}): (ctx: TContext) => string {
  return function buildTelegramAppMenuHtmlForContext(ctx) {
    return buildTelegramAppMenuHtml(
      deps.buildStatusHtml(ctx),
      deps.getPromptTemplateCommands?.(),
    );
  };
}

function getTelegramCommandErrorMessage(error: unknown): string {
  return error instanceof Error ? error.message : String(error);
}

export function parseTelegramCommand(
  text: string,
): ParsedTelegramCommand | undefined {
  const trimmed = text.trim();
  if (!trimmed.startsWith("/")) return undefined;
  const [head, ...tail] = trimmed.split(/\s+/);
  const name = head.slice(1).split("@")[0]?.toLowerCase();
  if (!name) return undefined;
  return { name, args: tail.join(" ").trim() };
}

export const TELEGRAM_COMMAND_ACTIONS = {
  stop: { kind: "stop", executionMode: "immediate" },
  abort: { kind: "abort", executionMode: "immediate" },
  next: { kind: "next", executionMode: "immediate" },
  continue: { kind: "continue", executionMode: "immediate" },
  status: { kind: "status", executionMode: "immediate" },
  queue: { kind: "queue", executionMode: "immediate" },
  compact: { kind: "compact", executionMode: "immediate" },
  new: { kind: "new", executionMode: "immediate" },
  model: { kind: "model", executionMode: "immediate" },
  thinking: { kind: "thinking", executionMode: "immediate" },
  settings: { kind: "settings", executionMode: "immediate" },
  help: { kind: "help", commandName: "help", executionMode: "immediate" },
  start: { kind: "help", commandName: "start", executionMode: "immediate" },
} as const satisfies Record<TelegramReservedCommandName, TelegramCommandAction>;

export function buildTelegramCommandAction(
  commandName: string | undefined,
): TelegramCommandAction {
  if (!isTelegramReservedCommandName(commandName)) {
    return { kind: "ignore", executionMode: "ignored" };
  }
  return TELEGRAM_COMMAND_ACTIONS[commandName];
}

export function getTelegramCommandExecutionMode(
  action: TelegramCommandAction,
): TelegramCommandExecutionMode {
  return action.executionMode;
}

function formatTelegramQueuedTurnCount(count: number): string {
  return count === 1 ? "1 queued turn" : `${count} queued turns`;
}

export async function handleTelegramStopCommand(
  deps: TelegramStopCommandDeps,
): Promise<void> {
  deps.clearPendingModelSwitch();
  const clearedCount = deps.clearQueuedTelegramItems();
  deps.setPreserveQueuedTurnsAsHistory(false);
  if (!deps.hasAbortHandler()) {
    const clearedSuffix =
      clearedCount > 0
        ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
        : "";
    if (clearedCount > 0) deps.updateStatus();
    await deps.sendTextReply(`No active turn.${clearedSuffix}`);
    return;
  }
  deps.abortCurrentTurn();
  deps.updateStatus();
  const clearedSuffix =
    clearedCount > 0
      ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
      : "";
  await deps.sendTextReply(`Aborted current turn.${clearedSuffix}`);
}

export async function handleTelegramAbortCommand(deps: {
  hasAbortHandler: () => boolean;
  clearPendingModelSwitch: () => void;
  abortCurrentTurn: () => void;
  setPreserveForIdle: () => void;
  updateStatus: () => void;
  sendTextReply: (text: string) => Promise<void>;
}): Promise<void> {
  deps.clearPendingModelSwitch();
  if (!deps.hasAbortHandler()) {
    await deps.sendTextReply("No active turn.");
    return;
  }
  deps.setPreserveForIdle();
  deps.abortCurrentTurn();
  deps.updateStatus();
  await deps.sendTextReply("Aborted current turn.");
}

export async function handleTelegramNextCommand(deps: {
  hasAbortHandler: () => boolean;
  isIdle: () => boolean;
  hasQueuedItems: () => boolean;
  clearPendingModelSwitch: () => void;
  abortCurrentTurn: () => void;
  dispatchNextQueuedTurn: () => void;

[420 more lines in file. Use offset=781 to continue.]


  setPreserveForDispatch: () => void;
  updateStatus: () => void;
  sendTextReply: (text: string) => Promise<void>;
}): Promise<void> {
  deps.clearPendingModelSwitch();
  if (!deps.hasQueuedItems()) {
    await deps.sendTextReply("<b>Queue is empty.</b>");
    return;
  }
  if (!deps.isIdle() && deps.hasAbortHandler()) {
    deps.setPreserveForDispatch();
    deps.abortCurrentTurn();
    deps.updateStatus();
    await deps.sendTextReply(
      "Aborted current turn. Dispatching next queued turn.",
    );
    return;
  }
  if (!deps.isIdle()) {
    await deps.sendTextReply("π is busy. Send /abort or /stop first.");
    return;
  }
  deps.dispatchNextQueuedTurn();
  deps.updateStatus();
  await deps.sendTextReply("Dispatching next queued turn.");
}

export async function handleTelegramContinueCommand<TMessage, TContext>(
  message: TMessage,
  ctx: TContext,
  deps: {
    enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise<void>;
  },
): Promise<void> {
  await deps.enqueueContinueTurn(message, ctx);
}

export async function handleTelegramCompactCommand(
  deps: TelegramCompactCommandDeps,
): Promise<void> {
  if (
    !deps.isIdle() ||
    deps.hasPendingMessages() ||
    deps.hasActiveTelegramTurn() ||
    deps.hasDispatchPending() ||
    deps.hasQueuedTelegramItems() ||
    deps.isCompactionInProgress()
  ) {
    await deps.sendTextReply(
      "Cannot compact while π or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
    );
    return;
  }
  deps.setCompactionInProgress(true);
  deps.updateStatus();
  try {
    deps.compact({
      onComplete: () => {
        deps.setCompactionInProgress(false);
        deps.updateStatus();
        deps.dispatchNextQueuedTelegramTurn();
        void deps.sendTextReply("Compaction completed.");
      },
      onError: (error) => {
        deps.setCompactionInProgress(false);
        deps.updateStatus();
        deps.dispatchNextQueuedTelegramTurn();
        deps.recordRuntimeEvent?.("compact", error);
        const errorMessage = getTelegramCommandErrorMessage(error);
        void deps.sendTextReply(`Compaction failed: ${errorMessage}`);
      },
    });
  } catch (error) {
    deps.setCompactionInProgress(false);
    deps.updateStatus();
    deps.recordRuntimeEvent?.("compact", error);
    const errorMessage = getTelegramCommandErrorMessage(error);
    await deps.sendTextReply(`Compaction failed: ${errorMessage}`);
    return;
  }
  await deps.sendTextReply("Compaction started.");
}

export async function handleTelegramNewCommand(deps: {
  sendTextReply: (text: string) => Promise<void>;
  sendUserMessage: (message: string, options?: { deliverAs?: "steer" | "followUp" }) => void;
}): Promise<void> {
  await deps.sendTextReply("Creating new session…");
  deps.sendUserMessage("/new", { deliverAs: "followUp" });
}

export async function handleTelegramStatusCommand<TContext>(deps: {
  ctx: TContext;
  showStatus: (ctx: TContext) => Promise<void>;
}): Promise<void> {
  await deps.showStatus(deps.ctx);
}

export async function handleTelegramModelCommand<TContext>(deps: {
  ctx: TContext;
  openModelMenu: (ctx: TContext) => Promise<void>;
}): Promise<void> {
  await deps.openModelMenu(deps.ctx);
}

export async function executeTelegramCommandAction<TMessage, TContext>(
  action: TelegramCommandAction,
  message: TMessage,
  ctx: TContext,
  deps: TelegramCommandActionDeps<TMessage, TContext>,
): Promise<boolean> {
  switch (action.kind) {
    case "ignore":
      return false;
    case "stop":
      await deps.handleStop(message, ctx);
      return true;
    case "abort":
      await deps.handleAbort(message, ctx);
      return true;
    case "next":
      await deps.handleNext(message, ctx);
      return true;
    case "continue":
      await deps.handleContinue(message, ctx);
      return true;
    case "queue":
      await deps.handleQueue(message, ctx);
      return true;
    case "compact":
      await deps.handleCompact(message, ctx);
      return true;
    case "new":
      await deps.handleNew(message, ctx);
      return true;
    case "status":
      await deps.handleStatus(message, ctx);
      return true;
    case "model":
      await deps.handleModel(message, ctx);
      return true;
    case "thinking":
      await deps.handleThinking(message, ctx);
      return true;
    case "settings":
      if (!deps.handleSettings) return false;
      await deps.handleSettings(message, ctx);
      return true;
    case "help":
      await deps.handleHelp(message, action.commandName, ctx);
      return true;
  }
}

export interface TelegramCommandHandlerTargetRuntimeDeps<
  TMessage extends TelegramCommandRuntimeMessage,
  TContext,
>
  extends
    Omit<
      TelegramCommandRuntimeDeps<TMessage, TContext>,
      | "enqueueControlItem"
      | "showStatus"
      | "openModelMenu"
      | "openSettingsMenu"
      | "sendTextReply"
      | "registerBotCommands"
    >,
    Omit<TelegramCommandTargetQueueRuntimeDeps<TContext>, "createControlItem">,
    TelegramBotCommandRegistrationDeps {
  allocateItemOrder: () => number;
  allocateControlOrder: () => number;
}

export function createTelegramCommandHandlerTargetRuntime<
  TMessage extends TelegramCommandRuntimeMessage,
  TContext,
>(
  deps: TelegramCommandHandlerTargetRuntimeDeps<TMessage, TContext>,
): (
  commandName: string | undefined,
  message: TMessage,
  ctx: TContext,
) => Promise<boolean> {
  const commandTargetRuntime = createTelegramCommandTargetQueueRuntime<
    TMessage,
    TContext
  >({
    createControlItem: createTelegramControlItemBuilder<TContext>({
      allocateItemOrder: deps.allocateItemOrder,
      allocateControlOrder: deps.allocateControlOrder,
    }),
    appendControlItem: deps.appendControlItem,
    dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
    showStatus: deps.showStatus,
    openModelMenu: deps.openModelMenu,
    openSettingsMenu: deps.openSettingsMenu,
    sendTextReply: deps.sendTextReply,
  });
  return createTelegramCommandHandler({
    hasAbortHandler: deps.hasAbortHandler,
    clearPendingModelSwitch: deps.clearPendingModelSwitch,
    hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
    clearQueuedTelegramItems: deps.clearQueuedTelegramItems,
    setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
    abortCurrentTurn: deps.abortCurrentTurn,
    isIdle: deps.isIdle,
    hasPendingMessages: deps.hasPendingMessages,
    hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
    hasDispatchPending: deps.hasDispatchPending,
    isCompactionInProgress: deps.isCompactionInProgress,
    setCompactionInProgress: deps.setCompactionInProgress,
    updateStatus: deps.updateStatus,
    dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
    enqueueContinueTurn: deps.enqueueContinueTurn,
    compact: deps.compact,
    enqueueControlItem: commandTargetRuntime.enqueueControlItem,
    showStatus: commandTargetRuntime.showStatus,
    openModelMenu: commandTargetRuntime.openModelMenu,
    openThinkingMenu: deps.openThinkingMenu,
    openQueueMenu: deps.openQueueMenu,
    openSettingsMenu: commandTargetRuntime.openSettingsMenu,
    getAllowedUserId: deps.getAllowedUserId,
    setAllowedUserId: deps.setAllowedUserId,
    registerBotCommands: createTelegramBotCommandRegistrar({
      setMyCommands: deps.setMyCommands,
    }),
    persistConfig: deps.persistConfig,
    sendTextReply: commandTargetRuntime.sendTextReply,
    sendUserMessage: deps.sendUserMessage,
    recordRuntimeEvent: deps.recordRuntimeEvent,
  });
}

export function createTelegramCommandHandler<
  TMessage extends TelegramCommandRuntimeMessage,
  TContext,
>(deps: TelegramCommandRuntimeDeps<TMessage, TContext>) {
  return async function handleTelegramCommand(
    commandName: string | undefined,
    message: TMessage,
    ctx: TContext,
  ): Promise<boolean> {
    return handleTelegramCommandRuntime(commandName, message, ctx, deps);
  };
}

export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
  deps: TelegramCommandOrPromptRuntimeDeps<TMessage, TContext>,
) {
  return {
    dispatchMessages: async (
      messages: TMessage[],
      ctx: TContext,
    ): Promise<void> => {
      const firstMessage = messages[0];
      if (!firstMessage) return;
      const command = parseTelegramCommand(deps.extractRawText(messages));
      const handled = await deps.handleCommand(
        command?.name,

[160 more lines in file. Use offset=1041 to continue.]


        firstMessage,
        ctx,
      );
      if (handled) return;
      if (command?.name && deps.expandPromptTemplateCommand) {
        const expanded = deps.expandPromptTemplateCommand(
          command.name,
          command.args,
        );
        if (expanded !== undefined) {
          await deps.enqueueTurn(
            [
              deps.replaceMessageText(firstMessage, expanded),
              ...messages.slice(1),
            ],
            ctx,
          );
          return;
        }
      }
      await deps.enqueueTurn(messages, ctx);
    },
  };
}

async function handleTelegramCommandRuntime<
  TMessage extends TelegramCommandRuntimeMessage,
  TContext,
>(
  commandName: string | undefined,
  message: TMessage,
  ctx: TContext,
  deps: TelegramCommandRuntimeDeps<TMessage, TContext>,
): Promise<boolean> {
  const sendReplyFor = (nextMessage: TMessage) => (text: string) =>
    deps.sendTextReply(nextMessage, text);
  const updateStatusFor = (commandCtx: TContext) => () =>
    deps.updateStatus(commandCtx);
  return executeTelegramCommandAction(
    buildTelegramCommandAction(commandName),
    message,
    ctx,
    {
      handleStop: async (nextMessage, commandCtx) => {
        await handleTelegramStopCommand({
          hasAbortHandler: deps.hasAbortHandler,
          clearPendingModelSwitch: deps.clearPendingModelSwitch,
          clearQueuedTelegramItems: () =>
            deps.clearQueuedTelegramItems(commandCtx),
          setPreserveQueuedTurnsAsHistory: deps.setPreserveQueuedTurnsAsHistory,
          abortCurrentTurn: deps.abortCurrentTurn,
          updateStatus: updateStatusFor(commandCtx),
          sendTextReply: sendReplyFor(nextMessage),
        });
      },
      handleAbort: async (nextMessage, commandCtx) => {
        await handleTelegramAbortCommand({
          hasAbortHandler: deps.hasAbortHandler,
          clearPendingModelSwitch: deps.clearPendingModelSwitch,
          abortCurrentTurn: deps.abortCurrentTurn,
          setPreserveForIdle: () => deps.setPreserveQueuedTurnsAsHistory(true),
          updateStatus: updateStatusFor(commandCtx),
          sendTextReply: sendReplyFor(nextMessage),
        });
      },
      handleNext: async (nextMessage, commandCtx) => {
        await handleTelegramNextCommand({
          hasAbortHandler: deps.hasAbortHandler,
          isIdle: () => deps.isIdle(commandCtx),
          hasQueuedItems: deps.hasQueuedTelegramItems,
          clearPendingModelSwitch: deps.clearPendingModelSwitch,
          abortCurrentTurn: deps.abortCurrentTurn,
          dispatchNextQueuedTurn: () =>
            deps.dispatchNextQueuedTelegramTurn(commandCtx),
          setPreserveForDispatch: () =>
            deps.setPreserveQueuedTurnsAsHistory(false),
          updateStatus: updateStatusFor(commandCtx),
          sendTextReply: sendReplyFor(nextMessage),
        });
      },
      handleContinue: async (nextMessage, commandCtx) => {
        await handleTelegramContinueCommand(nextMessage, commandCtx, {
          enqueueContinueTurn: deps.enqueueContinueTurn,
        });
      },
      handleQueue: async (nextMessage, commandCtx) => {
        await deps.openQueueMenu(nextMessage, commandCtx);
      },
      handleCompact: async (nextMessage, commandCtx) => {
        await handleTelegramCompactCommand({
          isIdle: () => deps.isIdle(commandCtx),
          hasPendingMessages: () => deps.hasPendingMessages(commandCtx),
          hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
          hasDispatchPending: deps.hasDispatchPending,
          hasQueuedTelegramItems: deps.hasQueuedTelegramItems,
          isCompactionInProgress: deps.isCompactionInProgress,
          setCompactionInProgress: deps.setCompactionInProgress,
          updateStatus: updateStatusFor(commandCtx),
          dispatchNextQueuedTelegramTurn: () =>
            deps.dispatchNextQueuedTelegramTurn(commandCtx),
          compact: (callbacks) => deps.compact(commandCtx, callbacks),
          sendTextReply: sendReplyFor(nextMessage),
          recordRuntimeEvent: deps.recordRuntimeEvent,
        });
      },
      handleNew: async (nextMessage, _commandCtx) => {
        if (!deps.sendUserMessage) {
          await sendReplyFor(nextMessage)(
            "New session not available in this runtime.",
          );
          return;
        }
        await handleTelegramNewCommand({
          sendTextReply: sendReplyFor(nextMessage),
          sendUserMessage: deps.sendUserMessage,
        });
      },
      handleStatus: async (nextMessage, commandCtx) => {
        await deps.showStatus(nextMessage, commandCtx);
      },
      handleModel: async (nextMessage, commandCtx) => {
        await handleTelegramModelCommand<TContext>({
          ctx: commandCtx,
          openModelMenu: (controlCtx) =>
            deps.openModelMenu(nextMessage, controlCtx),
        });
      },
      handleThinking: async (nextMessage, commandCtx) => {
        await deps.openThinkingMenu(nextMessage, commandCtx);
      },
      handleSettings: deps.openSettingsMenu
        ? async (nextMessage, commandCtx) => {
            await deps.openSettingsMenu?.(nextMessage, commandCtx);
          }
        : undefined,
      handleHelp: async (nextMessage, _nextCommandName, commandCtx) => {
        try {
          await deps.registerBotCommands();
        } catch (error) {
          const errorMessage = getTelegramCommandErrorMessage(error);
          await deps.sendTextReply(
            nextMessage,
            `Warning: failed to register bot commands menu: ${errorMessage}`,
          );
        }
        if (nextMessage.from?.id !== undefined) {
          await pairTelegramUserIfNeeded(nextMessage.from.id, {
            allowedUserId: deps.getAllowedUserId(),
            ctx: undefined,
            setAllowedUserId: deps.setAllowedUserId,
            persistConfig: deps.persistConfig,
            updateStatus: updateStatusFor(commandCtx),
          });
        }
        await deps.showStatus(nextMessage, commandCtx);
      },
    },
  );
}


bash

cd /mnt/rpi/dev/pi-telegram-sebas && rg -n "sessionManager|getEntries|switch session|resume session|previous session|select session|session entry" lib tests -g '!**/node_modules/**'

cd /home/sebas && rg -n "sessionManager|getEntries|switchSession|resume" ~/.npm-global/lib ~/.npm-global/bin /mnt/rpi/dev -g '!**/node_modules/**'


lib/status.ts:43:  sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
lib/status.ts:500:  for (const entry of ctx.sessionManager.getEntries()) {
tests/status.test.ts:328:    sessionManager: { getEntries: () => [] },
tests/status.test.ts:343:    sessionManager: { getEntries: () => [] },
tests/runtime.test.ts:173:    sessionManager: {
tests/runtime.test.ts:174:      getEntries: () => [],
tests/runtime.test.ts:916:      sessionManager: {
tests/runtime.test.ts:917:        getEntries: () => [],
tests/runtime.test.ts:1056:      sessionManager: {
tests/runtime.test.ts:1057:        getEntries: () => [],


/mnt/rpi/dev/pi-telegram-sebas/CHANGELOG.md:151:- `[Locks]` Added the shared `locks.json` singleton ownership standard and documented it in `docs/locks.md`. Telegram polling ownership now lives under `@llblab/pi-telegram` in `~/.pi/agent/locks.json`, while `telegram.json` remains pure bot/user configuration. `/telegram-connect` acquires the lock, replaces stale locks, or moves live external owners here through an interactive confirmation; `/telegram-disconnect` releases it. Session initialization reads ownership state, active owners stop local polling when `locks.json` no longer points at their own `pid`/`cwd`, session replacement via `/new` suspends polling/watchers without touching explicit ownership before resuming in the new session, and a reopened pi process resumes a stale same-`cwd` lock automatically. Impact: runtime locks can be reset or moved without deleting Telegram configuration, finding the previous pi instance, or crashing on stale extension contexts.
/mnt/rpi/dev/pi-telegram-sebas/lib/status.ts:43:  sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
/mnt/rpi/dev/pi-telegram-sebas/lib/status.ts:500:  for (const entry of ctx.sessionManager.getEntries()) {
/mnt/rpi/dev/pi-telegram-sebas/tests/model.test.ts:383:test("Model-switch continuation turn stays control-lane and resume-oriented", () => {
/mnt/rpi/dev/pi-telegram-sebas/tests/model.test.ts:472:test("Continuation prompt stays Telegram-scoped and resume-oriented", () => {
/mnt/rpi/dev/pi-telegram-sebas/tests/outbound-handlers.test.ts:406:test("Outbound comments resume after indented and longer closing fences", () => {
/mnt/rpi/dev/pi-telegram-sebas/tests/status.test.ts:328:    sessionManager: { getEntries: () => [] },
/mnt/rpi/dev/pi-telegram-sebas/tests/status.test.ts:343:    sessionManager: { getEntries: () => [] },
/mnt/rpi/dev/pi-telegram-sebas/tests/queue.test.ts:731:test("Agent end dispatch policy resumes after success and error, but not preserved aborts", () => {
/mnt/rpi/dev/pi-telegram-sebas/tests/runtime.test.ts:173:    sessionManager: {
/mnt/rpi/dev/pi-telegram-sebas/tests/runtime.test.ts:174:      getEntries: () => [],
/mnt/rpi/dev/pi-telegram-sebas/tests/runtime.test.ts:916:      sessionManager: {
/mnt/rpi/dev/pi-telegram-sebas/tests/runtime.test.ts:917:        getEntries: () => [],
/mnt/rpi/dev/pi-telegram-sebas/tests/runtime.test.ts:1056:      sessionManager: {
/mnt/rpi/dev/pi-telegram-sebas/tests/runtime.test.ts:1057:        getEntries: () => [],
/mnt/rpi/dev/pi-telegram-sebas/tests/locks.test.ts:290:test("Locked polling runtime resumes stale same-cwd ownership after process restart", async () => {
/mnt/rpi/dev/pi-telegram-sebas/docs/locks.md:67:Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live external owner, or replace a stale lock from another directory by themselves. Such locks should stay visible as state until the user runs the start/connect command. Session replacement should suspend local runtime work and ownership watchers without releasing the lock, so the next session in the same `pid`/`cwd` can resume from explicit ownership.
/mnt/rpi/dev/pi-telegram-sebas/docs/architecture.md:68:Telegram bot configuration stays in `~/.pi/agent/telegram.json`; singleton runtime ownership lives separately in `~/.pi/agent/locks.json` under `@llblab/pi-telegram`. `/telegram-connect` acquires or moves that lock before polling starts, and `/telegram-disconnect` stops polling and releases it. Session start may read the existing lock and resume polling when the lock already points at the current `pid`/`cwd`; after a full π process restart, it may also replace a stale lock from the same `cwd` and resume polling automatically. Session start does not create new ownership from an inactive lock, a live external lock, or a stale lock from another directory. Session replacement suspends polling and ownership watchers without releasing the lock, allowing the next session-start hook in the same `pid`/`cwd` to resume from the existing explicit ownership. When a live external owner exists, `/telegram-connect` asks whether to move singleton ownership to the current π instance. Active owners poll the lock while running through a snapshotted ownership context, so long-lived timers do not touch stale π contexts after `/new`; they stop local polling when `locks.json` no longer points at their own `pid`/`cwd`, without deleting the new owner lock. Deleting `locks.json` resets runtime ownership without deleting Telegram configuration.


/**
 * pi SDK adapter boundary
 * Zones: pi agent sdk boundary, shared adapters
 * Owns direct pi SDK imports and exposes narrow bridge-facing helpers/types for the extension composition layer
 */

import {
  type AgentEndEvent,
  type AgentStartEvent,
  type BeforeAgentStartEvent,
  type ExtensionAPI,
  type ExtensionCommandContext,
  type ExtensionContext,
  type SessionShutdownEvent,
  type SessionStartEvent,
  type SlashCommandInfo,
  SettingsManager,
} from "@mariozechner/pi-coding-agent";

export type {
  AgentEndEvent,
  AgentStartEvent,
  BeforeAgentStartEvent,
  ExtensionAPI,
  ExtensionCommandContext,
  ExtensionContext,
  SessionShutdownEvent,
  SessionStartEvent,
  SlashCommandInfo,
};

export interface PiSettingsManager {
  reload: () => Promise<void>;
  flush: () => Promise<void>;
  getEnabledModels: () => string[] | undefined;
  setEnabledModels: (patterns: string[] | undefined) => void;
}

export type PiSlashCommandInfo = SlashCommandInfo;

export interface PiExtensionApiRuntimePorts {
  sendUserMessage: ExtensionAPI["sendUserMessage"];
  exec: ExtensionAPI["exec"];
  getCommands: ExtensionAPI["getCommands"];
  getThinkingLevel: ExtensionAPI["getThinkingLevel"];
  setThinkingLevel: ExtensionAPI["setThinkingLevel"];
  setModel: ExtensionAPI["setModel"];
}

export function createExtensionApiRuntimePorts(
  api: Pick<
    ExtensionAPI,
    | "sendUserMessage"
    | "exec"
    | "getCommands"
    | "getThinkingLevel"
    | "setThinkingLevel"
    | "setModel"
  >,
): PiExtensionApiRuntimePorts {
  return {
    sendUserMessage: (content) => api.sendUserMessage(content),
    exec: (command, args, options) => api.exec(command, args, options),
    getCommands: () => api.getCommands(),
    getThinkingLevel: () => api.getThinkingLevel(),
    setThinkingLevel: (level) => api.setThinkingLevel(level),
    setModel: (model) => api.setModel(model),
  };
}

export function createSettingsManager(cwd: string): PiSettingsManager {
  return SettingsManager.create(cwd);
}

export function createScopedModelPatternPersister(deps: {
  createSettingsManager: (cwd: string) => PiSettingsManager;
  clearCachedModelMenuInputs: () => void;
}): (patterns: string[], ctx: ExtensionContext) => Promise<void> {
  return async function persistScopedModelPatterns(patterns, ctx) {
    const settingsManager = deps.createSettingsManager(ctx.cwd);
    settingsManager.setEnabledModels(
      patterns.length > 0 ? patterns : undefined,
    );
    await settingsManager.flush();
    deps.clearCachedModelMenuInputs();
  };
}

export function getExtensionContextModel(
  ctx: ExtensionContext,
): ExtensionContext["model"] {
  return ctx.model;
}

export function getExtensionContextCwd(ctx: ExtensionContext): string {
  return ctx.cwd;
}

export function isExtensionContextIdle(ctx: ExtensionContext): boolean {
  return ctx.isIdle();
}

export function hasExtensionContextPendingMessages(
  ctx: ExtensionContext,
): boolean {
  return ctx.hasPendingMessages();
}

export function compactExtensionContext(
  ctx: ExtensionContext,
  callbacks: Parameters<ExtensionContext["compact"]>[0],
): ReturnType<ExtensionContext["compact"]> {
  return ctx.compact(callbacks);
}


cd /home/sebas && rg -n "interface SessionManager|getEntries\(|switch.*session|resume.*session|SessionEntry|sessionManager" $(npm root -g 2>/dev/null) ~/.npm-global/lib/node_modules /mnt/rpi/dev/pi-coding-agent -g '!**/node_modules/**'


rg: /mnt/rpi/dev/pi-coding-agent: No such file or directory (os error 2)

Command exited with code 2


node -e "const p=require.resolve('@mariozechner/pi-coding-agent'); console.log(p)"


node:internal/modules/cjs/loader:1386
  throw err;
  ^

Error: Cannot find module '@mariozechner/pi-coding-agent'
Require stack:
- /home/sebas/[eval]
    at Function._resolveFilename (node:internal/modules/cjs/loader:1383:15)
    at Function.resolve (node:internal/modules/helpers:157:19)
    at [eval]:1:17
    at runScriptInThisContext (node:internal/vm:209:10)
    at node:internal/process/execution:446:12
    at [eval]-wrapper:6:24
    at runScriptInContext (node:internal/process/execution:444:60)
    at evalFunction (node:internal/process/execution:279:30)
    at evalTypeScript (node:internal/process/execution:291:3)
    at node:internal/main/eval_string:74:3 {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ '/home/sebas/[eval]' ]
}

Node.js v22.22.2

Command exited with code 1


cd /mnt/rpi/dev/pi-telegram-sebas && node -e "const p=require.resolve('@mariozechner/pi-coding-agent'); console.log(p)"


node:internal/modules/cjs/loader:657
      throw e;
      ^

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: No "exports" main defined in /mnt/rpi/dev/pi-telegram-sebas/node_modules/@mariozechner/pi-coding-agent/package.json
    at exportsNotFound (node:internal/modules/esm/resolve:314:10)
    at packageExportsResolve (node:internal/modules/esm/resolve:604:13)
    at resolveExports (node:internal/modules/cjs/loader:650:36)
    at Function._findPath (node:internal/modules/cjs/loader:717:31)
    at Function._resolveFilename (node:internal/modules/cjs/loader:1369:27)
    at Function.resolve (node:internal/modules/helpers:157:19)
    at [eval]:1:17
    at runScriptInThisContext (node:internal/vm:209:10)
    at node:internal/process/execution:446:12
    at [eval]-wrapper:6:24 {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

Node.js v22.22.2

Command exited with code 1


cd /mnt/rpi/dev/pi-telegram-sebas/node_modules/@mariozechner/pi-coding-agent && find . -maxdepth 2 | sort | sed -n '1,120p'


.
./CHANGELOG.md
./dist
./dist/bun
./dist/cli
./dist/cli.d.ts
./dist/cli.d.ts.map
./dist/cli.js
./dist/cli.js.map
./dist/config.d.ts
./dist/config.d.ts.map
./dist/config.js
./dist/config.js.map
./dist/core
./dist/index.d.ts
./dist/index.d.ts.map
./dist/index.js
./dist/index.js.map
./dist/main.d.ts
./dist/main.d.ts.map
./dist/main.js
./dist/main.js.map
./dist/migrations.d.ts
./dist/migrations.d.ts.map
./dist/migrations.js
./dist/migrations.js.map
./dist/modes
./dist/package-manager-cli.d.ts
./dist/package-manager-cli.d.ts.map
./dist/package-manager-cli.js
./dist/package-manager-cli.js.map
./dist/utils
./docs
./docs/compaction.md
./docs/custom-provider.md
./docs/development.md
./docs/extensions.md
./docs/images
./docs/json.md
./docs/keybindings.md
./docs/models.md
./docs/packages.md
./docs/prompt-templates.md
./docs/providers.md
./docs/rpc.md
./docs/sdk.md
./docs/session.md
./docs/settings.md
./docs/shell-aliases.md
./docs/skills.md
./docs/terminal-setup.md
./docs/termux.md
./docs/themes.md
./docs/tmux.md
./docs/tree.md
./docs/tui.md
./docs/windows.md
./examples
./examples/extensions
./examples/README.md
./examples/rpc-extension-ui.ts
./examples/sdk
./package.json
./README.md


cd /mnt/rpi/dev/pi-telegram-sebas/node_modules/@mariozechner/pi-coding-agent && rg -n "sessionManager|getEntries|resume|switch.*session|listSessions|SessionEntry|rename session|restore" dist/*.d.ts dist/**/*.d.ts docs/session.md docs/sdk.md


docs/session.md:17:Pi also supports deleting sessions interactively from `/resume` (select a session and press `Ctrl+D`, then confirm). When available, pi uses the `trash` CLI to avoid permanent deletion.
docs/session.md:173:All entries (except `SessionHeader`) extend `SessionEntryBase`:
docs/session.md:176:interface SessionEntryBase {
docs/session.md:212:Emitted when the user switches models mid-session.
docs/session.md:291:The session name is displayed in the session selector (`/resume`) instead of the first message when set.
docs/session.md:405:- `getEntries()` - All entries (excluding header)
docs/sdk.md:26:  sessionManager: SessionManager.inMemory(),
docs/sdk.md:66:  sessionManager: SessionManager.inMemory(),
docs/sdk.md:118:Session replacement APIs such as new-session, resume, fork, and import live on `AgentSessionRuntime`, not on `AgentSession`.
docs/sdk.md:137:const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
docs/sdk.md:142:      sessionManager,
docs/sdk.md:153:  sessionManager: SessionManager.create(process.cwd()),
docs/sdk.md:382:1. Tries to restore from session (if continuing)
docs/sdk.md:404:  sessionManager: SessionManager.inMemory(),
docs/sdk.md:417:  sessionManager: SessionManager.inMemory(),
docs/sdk.md:675:  sessionManager: SessionManager.inMemory(),
docs/sdk.md:680:  sessionManager: SessionManager.create(process.cwd()),
docs/sdk.md:685:  sessionManager: SessionManager.continueRecent(process.cwd()),
docs/sdk.md:693:  sessionManager: SessionManager.open("/path/to/session.jsonl"),
docs/sdk.md:700:// Session replacement API for /new, /resume, /fork, and import flows.
docs/sdk.md:701:const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
docs/sdk.md:706:      sessionManager,
docs/sdk.md:717:  sessionManager: SessionManager.create(process.cwd()),
docs/sdk.md:724:await runtime.switchSession("/path/to/session.jsonl");
docs/sdk.md:740:const entries = sm.getEntries();        // All entries (excludes header)
docs/sdk.md:780:  sessionManager: SessionManager.inMemory(),
docs/sdk.md:845:  // Warning if session model couldn't be restored
docs/sdk.md:926:  sessionManager: SessionManager.inMemory(),
docs/sdk.md:958:const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
docs/sdk.md:961:    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),
docs/sdk.md:969:  sessionManager: SessionManager.create(process.cwd()),
docs/sdk.md:998:const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
docs/sdk.md:1001:    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),
docs/sdk.md:1009:  sessionManager: SessionManager.create(process.cwd()),
docs/sdk.md:1035:const createRuntime: CreateAgentSessionRuntimeFactory = async ({ cwd, sessionManager, sessionStartEvent }) => {
docs/sdk.md:1038:    ...(await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })),
docs/sdk.md:1046:  sessionManager: SessionManager.create(process.cwd()),
dist/core/session-manager.d.ts:17:export interface SessionEntryBase {
dist/core/session-manager.d.ts:23:export interface SessionMessageEntry extends SessionEntryBase {
dist/core/session-manager.d.ts:27:export interface ThinkingLevelChangeEntry extends SessionEntryBase {
dist/core/session-manager.d.ts:31:export interface ModelChangeEntry extends SessionEntryBase {
dist/core/session-manager.d.ts:36:export interface CompactionEntry<T = unknown> extends SessionEntryBase {
dist/core/session-manager.d.ts:46:export interface BranchSummaryEntry<T = unknown> extends SessionEntryBase {
dist/core/session-manager.d.ts:65:export interface CustomEntry<T = unknown> extends SessionEntryBase {
dist/core/session-manager.d.ts:71:export interface LabelEntry extends SessionEntryBase {
dist/core/session-manager.d.ts:77:export interface SessionInfoEntry extends SessionEntryBase {
dist/core/session-manager.d.ts:93:export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
dist/core/session-manager.d.ts:101:export type SessionEntry = SessionMessageEntry | ThinkingLevelChangeEntry | ModelChangeEntry | CompactionEntry | BranchSummaryEntry | CustomEntry | CustomMessageEntry | LabelEntry | SessionInfoEntry;
dist/core/session-manager.d.ts:103:export type FileEntry = SessionHeader | SessionEntry;
dist/core/session-manager.d.ts:106:    entry: SessionEntry;
dist/core/session-manager.d.ts:136:export type ReadonlySessionManager = Pick<SessionManager, "getCwd" | "getSessionDir" | "getSessionId" | "getSessionFile" | "getLeafId" | "getLeafEntry" | "getEntry" | "getLabel" | "getBranch" | "getHeader" | "getEntries" | "getTree" | "getSessionName">;
dist/core/session-manager.d.ts:141:export declare function getLatestCompactionEntry(entries: SessionEntry[]): CompactionEntry | null;
dist/core/session-manager.d.ts:147:export declare function buildSessionContext(entries: SessionEntry[], leafId?: string | null, byId?: Map<string, SessionEntry>): SessionContext;
dist/core/session-manager.d.ts:182:    /** Switch to a different session file (used for resume and branching) */
dist/core/session-manager.d.ts:192:    _persist(entry: SessionEntry): void;
dist/core/session-manager.d.ts:223:    getLeafEntry(): SessionEntry | undefined;
dist/core/session-manager.d.ts:224:    getEntry(id: string): SessionEntry | undefined;
dist/core/session-manager.d.ts:228:    getChildren(parentId: string): SessionEntry[];
dist/core/session-manager.d.ts:244:    getBranch(fromId?: string): SessionEntry[];
dist/core/session-manager.d.ts:259:    getEntries(): SessionEntry[];
dist/core/session-cwd.d.ts:10:export declare function getMissingSessionCwdIssue(sessionManager: SessionCwdSource, fallbackCwd: string): SessionCwdIssue | undefined;
dist/core/session-cwd.d.ts:17:export declare function assertSessionCwdExists(sessionManager: SessionCwdSource, fallbackCwd: string): void;
dist/core/sdk.d.ts:36:    sessionManager?: SessionManager;
dist/core/sdk.d.ts:48:    /** Warning if session was restored with a different model than saved */
dist/core/sdk.d.ts:88: *   sessionManager: SessionManager.inMemory(),
dist/core/model-resolver.d.ts:106:export declare function restoreModelFromSession(savedProvider: string, savedModelId: string, currentModel: Model<Api> | undefined, shouldPrintMessages: boolean, modelRegistry: ModelRegistry): Promise<{
dist/core/output-guard.d.ts:2:export declare function restoreStdout(): void;
dist/core/model-registry.d.ts:92:     * built-in models overridden by this provider are restored to their original state.
dist/core/keybindings.d.ts:21:    "app.session.resume": true;
dist/core/keybindings.d.ts:234:    readonly "app.session.resume": {
dist/core/footer-data-provider.d.ts:3: * Token stats, model info available via ctx.sessionManager and ctx.model.
dist/core/agent-session-services.d.ts:46:    sessionManager: SessionManager;
dist/core/agent-session-runtime.d.ts:26:    sessionManager: SessionManager;
dist/core/agent-session-runtime.d.ts:52:    switchSession(sessionPath: string, cwdOverride?: string): Promise<{
dist/core/agent-session-runtime.d.ts:57:        setup?: (sessionManager: SessionManager) => Promise<void>;
dist/core/agent-session-runtime.d.ts:74: * later /new, /resume, /fork, and import flows.
dist/core/agent-session-runtime.d.ts:79:    sessionManager: SessionManager;
dist/core/agent-session.d.ts:70:    sessionManager: SessionManager;
dist/core/agent-session.d.ts:145:    readonly sessionManager: SessionManager;
dist/cli/args.d.ts:16:    resume?: boolean;
dist/cli/session-picker.d.ts:2: * TUI session selector for --resume flag
dist/index.d.ts:16:export { type BranchSummaryEntry, buildSessionContext, type CompactionEntry, CURRENT_SESSION_VERSION, type CustomEntry, type CustomMessageEntry, type FileEntry, getLatestCompactionEntry, type ModelChangeEntry, migrateSessionEntries, type NewSessionOptions, parseSessionEntries, type SessionContext, type SessionEntry, type SessionEntryBase, type SessionHeader, type SessionInfo, type SessionInfoEntry, SessionManager, type SessionMessageEntry, type ThinkingLevelChangeEntry, } from "./core/session-manager.js";


**Inspecting sdk types**

I need to take a closer look at the SDK types for ExtensionContext, as it might include something related to a runtime or session manager. It’s important to ensure I understand the types accurately, as they could be crucial for how I'm implementing functionality. I’ll focus on gathering details that clarify what the session manager entails and how it interacts with the runtime context. This might help me improve my overall understanding and usage of the SDK.

cd /mnt/rpi/dev/pi-telegram-sebas/node_modules/@mariozechner/pi-coding-agent && rg -n "interface ExtensionContext|type ExtensionContext|switchSession\(|sessionManager:" dist/index.d.ts dist/core/*.d.ts dist/**/*.d.ts


dist/core/index.d.ts:10:export { type AgentEndEvent, type AgentStartEvent, type AgentToolResult, type AgentToolUpdateCallback, type BeforeAgentStartEvent, type ContextEvent, defineTool, discoverAndLoadExtensions, type ExecOptions, type ExecResult, type Extension, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ExtensionError, type ExtensionEvent, type ExtensionFactory, type ExtensionFlag, type ExtensionHandler, ExtensionRunner, type ExtensionShortcut, type ExtensionUIContext, type LoadExtensionsResult, type MessageRenderer, type RegisteredCommand, type SessionBeforeCompactEvent, type SessionBeforeForkEvent, type SessionBeforeSwitchEvent, type SessionBeforeTreeEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SessionTreeEvent, type ToolCallEvent, type ToolCallEventResult, type ToolDefinition, type ToolRenderResultOptions, type ToolResultEvent, type TurnEndEvent, type TurnStartEvent, } from "./extensions/index.js";
dist/core/agent-session-services.d.ts:46:    sessionManager: SessionManager;
dist/core/sdk.d.ts:88: *   sessionManager: SessionManager.inMemory(),
dist/core/session-cwd.d.ts:10:export declare function getMissingSessionCwdIssue(sessionManager: SessionCwdSource, fallbackCwd: string): SessionCwdIssue | undefined;
dist/core/session-cwd.d.ts:17:export declare function assertSessionCwdExists(sessionManager: SessionCwdSource, fallbackCwd: string): void;
dist/core/agent-session-runtime.d.ts:26:    sessionManager: SessionManager;
dist/core/agent-session-runtime.d.ts:52:    switchSession(sessionPath: string, cwdOverride?: string): Promise<{
dist/core/agent-session-runtime.d.ts:57:        setup?: (sessionManager: SessionManager) => Promise<void>;
dist/core/agent-session-runtime.d.ts:79:    sessionManager: SessionManager;
dist/core/agent-session.d.ts:70:    sessionManager: SessionManager;
dist/core/agent-session.d.ts:145:    readonly sessionManager: SessionManager;
dist/core/agent-session.d.ts:70:    sessionManager: SessionManager;
dist/core/agent-session.d.ts:145:    readonly sessionManager: SessionManager;
dist/core/session-cwd.d.ts:10:export declare function getMissingSessionCwdIssue(sessionManager: SessionCwdSource, fallbackCwd: string): SessionCwdIssue | undefined;
dist/core/session-cwd.d.ts:17:export declare function assertSessionCwdExists(sessionManager: SessionCwdSource, fallbackCwd: string): void;
dist/core/index.d.ts:10:export { type AgentEndEvent, type AgentStartEvent, type AgentToolResult, type AgentToolUpdateCallback, type BeforeAgentStartEvent, type ContextEvent, defineTool, discoverAndLoadExtensions, type ExecOptions, type ExecResult, type Extension, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext, type ExtensionError, type ExtensionEvent, type ExtensionFactory, type ExtensionFlag, type ExtensionHandler, ExtensionRunner, type ExtensionShortcut, type ExtensionUIContext, type LoadExtensionsResult, type MessageRenderer, type RegisteredCommand, type SessionBeforeCompactEvent, type SessionBeforeForkEvent, type SessionBeforeSwitchEvent, type SessionBeforeTreeEvent, type SessionCompactEvent, type SessionShutdownEvent, type SessionStartEvent, type SessionTreeEvent, type ToolCallEvent, type ToolCallEventResult, type ToolDefinition, type ToolRenderResultOptions, type ToolResultEvent, type TurnEndEvent, type TurnStartEvent, } from "./extensions/index.js";
dist/core/agent-session-runtime.d.ts:26:    sessionManager: SessionManager;
dist/core/agent-session-runtime.d.ts:52:    switchSession(sessionPath: string, cwdOverride?: string): Promise<{
dist/core/agent-session-runtime.d.ts:57:        setup?: (sessionManager: SessionManager) => Promise<void>;
dist/core/agent-session-runtime.d.ts:79:    sessionManager: SessionManager;
dist/core/agent-session-services.d.ts:46:    sessionManager: SessionManager;
dist/core/sdk.d.ts:88: *   sessionManager: SessionManager.inMemory(),


/**
 * Extension system for lifecycle events and custom tools.
 */
export type { SlashCommandInfo, SlashCommandSource } from "../slash-commands.js";
export type { SourceInfo } from "../source-info.js";
export { createExtensionRuntime, discoverAndLoadExtensions, loadExtensionFromFactory, loadExtensions, } from "./loader.js";
export type { ExtensionErrorListener, ForkHandler, NavigateTreeHandler, NewSessionHandler, ShutdownHandler, SwitchSessionHandler, } from "./runner.js";
export { ExtensionRunner } from "./runner.js";
export type { AgentEndEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppendEntryHandler, AppKeybinding, BashToolCallEvent, BashToolResultEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, CompactOptions, ContextEvent, ContextEventResult, ContextUsage, CustomToolCallEvent, CustomToolResultEvent, EditToolCallEvent, EditToolResultEvent, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, FindToolResultEvent, GetActiveToolsHandler, GetAllToolsHandler, GetCommandsHandler, GetThinkingLevelHandler, GrepToolCallEvent, GrepToolResultEvent, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, LsToolResultEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ModelSelectEvent, ModelSelectSource, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, ReadToolResultEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, ResourcesDiscoverEvent, ResourcesDiscoverResult, SendMessageHandler, SendUserMessageHandler, SessionBeforeCompactEvent, SessionBeforeCompactResult, SessionBeforeForkEvent, SessionBeforeForkResult, SessionBeforeSwitchEvent, SessionBeforeSwitchResult, SessionBeforeTreeEvent, SessionBeforeTreeResult, SessionCompactEvent, SessionEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SetActiveToolsHandler, SetLabelHandler, SetModelHandler, SetThinkingLevelHandler, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, ToolResultEventResult, TreePreparation, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WriteToolCallEvent, WriteToolResultEvent, } from "./types.js";
export { defineTool, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, } from "./types.js";
export { wrapRegisteredTool, wrapRegisteredTools } from "./wrapper.js";
//# sourceMappingURL=index.d.ts.map


cd /mnt/rpi/dev/pi-telegram-sebas/node_modules/@mariozechner/pi-coding-agent && rg -n "interface ExtensionContext|type ExtensionContext =|switchSession\(|sessionManager" dist/core/extensions/types.d.ts


81:     * etc. are available via ctx.sessionManager and ctx.model.
180:export interface ExtensionContext {
188:    sessionManager: ReadonlySessionManager;
220:        setup?: (sessionManager: SessionManager) => Promise<void>;
238:    switchSession(sessionPath: string): Promise<{
1000:export interface ExtensionContextActions {
1019:        setup?: (sessionManager: SessionManager) => Promise<void>;


    percent: number | null;
}
export interface CompactOptions {
    customInstructions?: string;
    onComplete?: (result: CompactionResult) => void;
    onError?: (error: Error) => void;
}
/**
 * Context passed to extension event handlers.
 */
export interface ExtensionContext {
    /** UI methods for user interaction */
    ui: ExtensionUIContext;
    /** Whether UI is available (false in print/RPC mode) */
    hasUI: boolean;
    /** Current working directory */
    cwd: string;
    /** Session manager (read-only) */
    sessionManager: ReadonlySessionManager;
    /** Model registry for API key resolution */
    modelRegistry: ModelRegistry;
    /** Current model (may be undefined) */
    model: Model<any> | undefined;
    /** Whether the agent is idle (not streaming) */
    isIdle(): boolean;
    /** The current abort signal, or undefined when the agent is not streaming. */
    signal: AbortSignal | undefined;
    /** Abort the current agent operation */
    abort(): void;
    /** Whether there are queued messages waiting */
    hasPendingMessages(): boolean;
    /** Gracefully shutdown pi and exit. Available in all contexts. */
    shutdown(): void;
    /** Get current context usage for the active model. */
    getContextUsage(): ContextUsage | undefined;
    /** Trigger compaction without awaiting completion. */
    compact(options?: CompactOptions): void;
    /** Get the current effective system prompt. */
    getSystemPrompt(): string;
}
/**
 * Extended context for command handlers.
 * Includes session control methods only safe in user-initiated commands.
 */
export interface ExtensionCommandContext extends ExtensionContext {
    /** Wait for the agent to finish streaming */
    waitForIdle(): Promise<void>;
    /** Start a new session, optionally with initialization. */
    newSession(options?: {
        parentSession?: string;
        setup?: (sessionManager: SessionManager) => Promise<void>;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Fork from a specific entry, creating a new session file. */
    fork(entryId: string): Promise<{
        cancelled: boolean;
    }>;
    /** Navigate to a different point in the session tree. */
    navigateTree(targetId: string, options?: {
        summarize?: boolean;
        customInstructions?: string;
        replaceInstructions?: boolean;
        label?: string;
    }): Promise<{
        cancelled: boolean;
    }>;
    /** Switch to a different session file. */
    switchSession(sessionPath: string): Promise<{
        cancelled: boolean;
    }>;
    /** Reload extensions, skills, prompts, and themes. */
    reload(): Promise<void>;
}
/** Rendering options for tool results */
export interface ToolRenderResultOptions {
    /** Whether the result view is expanded */
    expanded: boolean;
    /** Whether this is a partial/streaming result */
    isPartial: boolean;
}
/** Context passed to tool renderers. */
export interface ToolRenderContext<TState = any, TArgs = any> {
    /** Current tool call arguments. Shared across call/result renders for the same tool call. */
    args: TArgs;
    /** Unique id for this tool execution. Stable across call/result renders for the same tool call. */
    toolCallId: string;
    /** Invalidate just this tool execution component for redraw. */
    invalidate: () => void;
    /** Previously returned component for this render slot, if any. */

[814 more lines in file. Use offset=260 to continue.]


    private leafId;
    private constructor();
    /** Switch to a different session file (used for resume and branching) */
    setSessionFile(sessionFile: string): void;
    newSession(options?: NewSessionOptions): string | undefined;
    private _buildIndex;
    private _rewriteFile;
    isPersisted(): boolean;
    getCwd(): string;
    getSessionDir(): string;
    getSessionId(): string;
    getSessionFile(): string | undefined;
    _persist(entry: SessionEntry): void;
    private _appendEntry;
    /** Append a message as child of current leaf, then advance leaf. Returns entry id.
     * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
     * Reason: we want these to be top-level entries in the session, not message session entries,
     * so it is easier to find them.
     * These need to be appended via appendCompaction() and appendBranchSummary() methods.
     */
    appendMessage(message: Message | CustomMessage | BashExecutionMessage): string;
    /** Append a thinking level change as child of current leaf, then advance leaf. Returns entry id. */
    appendThinkingLevelChange(thinkingLevel: string): string;
    /** Append a model change as child of current leaf, then advance leaf. Returns entry id. */
    appendModelChange(provider: string, modelId: string): string;
    /** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
    appendCompaction<T = unknown>(summary: string, firstKeptEntryId: string, tokensBefore: number, details?: T, fromHook?: boolean): string;
    /** Append a custom entry (for extensions) as child of current leaf, then advance leaf. Returns entry id. */
    appendCustomEntry(customType: string, data?: unknown): string;
    /** Append a session info entry (e.g., display name). Returns entry id. */
    appendSessionInfo(name: string): string;
    /** Get the current session name from the latest session_info entry, if any. */
    getSessionName(): string | undefined;
    /**
     * Append a custom message entry (for extensions) that participates in LLM context.
     * @param customType Extension identifier for filtering on reload
     * @param content Message content (string or TextContent/ImageContent array)
     * @param display Whether to show in TUI (true = styled display, false = hidden)
     * @param details Optional extension-specific metadata (not sent to LLM)
     * @returns Entry id
     */
    appendCustomMessageEntry<T = unknown>(customType: string, content: string | (TextContent | ImageContent)[], display: boolean, details?: T): string;
    getLeafId(): string | null;
    getLeafEntry(): SessionEntry | undefined;
    getEntry(id: string): SessionEntry | undefined;
    /**
     * Get all direct children of an entry.
     */
    getChildren(parentId: string): SessionEntry[];
    /**
     * Get the label for an entry, if any.
     */
    getLabel(id: string): string | undefined;
    /**
     * Set or clear a label on an entry.
     * Labels are user-defined markers for bookmarking/navigation.
     * Pass undefined or empty string to clear the label.
     */
    appendLabelChange(targetId: string, label: string | undefined): string;
    /**
     * Walk from entry to root, returning all entries in path order.
     * Includes all entry types (messages, compaction, model changes, etc.).
     * Use buildSessionContext() to get the resolved messages for the LLM.
     */
    getBranch(fromId?: string): SessionEntry[];
    /**
     * Build the session context (what gets sent to the LLM).
     * Uses tree traversal from current leaf.
     */
    buildSessionContext(): SessionContext;
    /**
     * Get session header.
     */
    getHeader(): SessionHeader | null;
    /**
     * Get all session entries (excludes header). Returns a shallow copy.
     * The session is append-only: use appendXXX() to add entries, branch() to
     * change the leaf pointer. Entries cannot be modified or deleted.
     */
    getEntries(): SessionEntry[];
    /**
     * Get the session as a tree structure. Returns a shallow defensive copy of all entries.
     * A well-formed session has exactly one root (first entry with parentId === null).
     * Orphaned entries (broken parent chain) are also returned as roots.
     */
    getTree(): SessionTreeNode[];
    /**
     * Start a new branch from an earlier entry.
     * Moves the leaf pointer to the specified entry. The next appendXXX() call
     * will create a child of that entry, forming a new branch. Existing entries
     * are not modified or deleted.
     */
    branch(branchFromId: string): void;
    /**
     * Reset the leaf pointer to null (before any entries).
     * The next appendXXX() call will create a new root entry (parentId = null).
     * Use this when navigating to re-edit the first user message.
     */
    resetLeaf(): void;
    /**
     * Start a new branch with a summary of the abandoned path.
     * Same as branch(), but also appends a branch_summary entry that captures
     * context from the abandoned conversation path.
     */
    branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromHook?: boolean): string;
    /**
     * Create a new session file containing only the path from root to the specified leaf.
     * Useful for extracting a single conversation path from a branched session.
     * Returns the new session file path, or undefined if not persisting.
     */
    createBranchedSession(leafId: string): string | undefined;
    /**
     * Create a new session.
     * @param cwd Working directory (stored in session header)
     * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
     */
    static create(cwd: string, sessionDir?: string): SessionManager;
    /**
     * Open a specific session file.
     * @param path Path to session file
     * @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
     * @param cwdOverride Optional cwd override instead of the session header cwd.
     */
    static open(path: string, sessionDir?: string, cwdOverride?: string): SessionManager;
    /**
     * Continue the most recent session, or create new if none.
     * @param cwd Working directory
     * @param sessionDir Optional session directory. If omitted, uses default (~/.pi/agent/sessions/<encoded-cwd>/).
     */
    static continueRecent(cwd: string, sessionDir?: string): SessionManager;
    /** Create an in-memory session (no file persistence) */
    static inMemory(cwd?: string): SessionManager;
    /**
     * Fork a session from another project directory into the current project.
     * Creates a new session in the target cwd with the full history from the source session.
     * @param sourcePath Path to the source session file
     * @param targetCwd Target working directory (where the new session will be stored)
     * @param sessionDir Optional session directory. If omitted, uses default for targetCwd.
     */
    static forkFrom(sourcePath: string, targetCwd: string, sessionDir?: string): SessionManager;

[14 more lines in file. Use offset=320 to continue.]


**Searching for session files**

I need to find a list of session files, which could possibly be in documents. My first step will be to search the session picker to locate them. It seems straightforward, but I want to make sure I'm thorough in my search. Let's see what I can uncover in the documents related to the sessions! It’s just a matter of looking in the right place to find what I’m after.

cd /mnt/rpi/dev/pi-telegram-sebas/node_modules/@mariozechner/pi-coding-agent && rg -n "session picker|list session|recent session|session files|continueRecent|getSessionDir\(|readdir.*session|sessionDir" dist docs/session.md docs/sdk.md