ЯзыкРусский

Удаленные сессии

Сохранить URL и токен с развернутым диапазоном с другого личного экземпляра AirCode Ø , а затем выбрать это соединение в Новая сессия → Запустить на. Удаленный экземпляр вызывает свой собственный подключенный Claude Code, Codex или другой доступный набор нацпровайдеров. Текущий клиент отображает разговор и терминал, когда работает удаленный агент.

Выберите место, где живет проект. С местоположение проекта → Этот инстанция, A владеет проектом P1 и B запускает агента с помощью учетной записи провайдера B. B запрашивает необходимые файлы через инструменты рабочего пространства; A читает, редактирует и запускает команды в своем фактическом каталоге P1. Нет клонирования или окончательной синхронизации. Запрошенный код и командный выход достигают B и его поставщика AI . Сведения о провайдере и учет пользования остаются на B. Зависимости, строения и испытания остаются на А.

A открывает аутентифицированное соединение WSS с B, которое переносит вызовы инструментов в обоих направлениях. Машины могут находиться в разных сетях или дата-центрах. A не нуждается в входящем порту или общественном адресе; B нуждается в доступном экземпляре HTTPS URL, и его прокси должен поддерживать обновления WebSocket. Оба случая должны поддерживать подключенные рабочие пространства. Закрытие браузера или мобильного просмотра оставляет это соединение с сервером на месте. Должен оставаться в действии для агента, чтобы использовать свой проект.

местоположение проекта → Удаленный случай сохраняет другой режим: и агент, и уполномоченный проект токена находятся на B. Эти два режима являются явными; выбор проекта с аналогичным названием на B никогда не означает P1 на A.

Подключите два ваших инстанции

  1. На удаленном примере подключите учетную запись вашего родного провайдера в AI Usage.
  2. Откройте Настройки → AI gateway, на мобильном устройстве Ещё → AI gateway. В разделе Создать доступ включите Разрешить приватные подключения. В разделе Создать токен оставьте Другой экземпляр, назовите соединение и нажмите Создать токен подключения. По умолчанию файлы и команды остаются на A, а агент использует учётную запись ИИ на B. Для работы с файлами B откройте Где работать с файлами и выполнять команды?, выберите Этот экземпляр и проект на B. Выбирать для каждого сеанса позволяет оба расположения с той же границей проекта на B.
  3. Копируйте токен из его однократного раскрытия и отображаемого Base URL.
  4. На A откройте Настройки → AI gateway → Добавить другой сервер. Введите имя соединения, URL экземпляра B или его Base URL и токен, затем нажмите Сохранить соединение. Подключение также доступно через Новый сеанс → Запустить на → Добавить удалённый экземпляр….
  5. В Новая сессиявыберите сохраненное соединение в разделе Run on. Проверьте его местоположение проекта по умолчанию Этот случай когда токен позволяет это сделать. Сохраните или выберите P1 под Project на этом примере, выберите Harness → Model → Усилие → Разрешения, введите запрос и выберите Начало сессии. Текущие адаптеры рабочего пространства поддерживают Claude Code и Codex; доступны варианты приходят из каталога B. Другие двигатели сохраняют режим удаленного проекта.

Токен отправляется один раз на текущий сервер для шифрования хранения. Веб-клиенты и входные клиенты впоследствии используют идентификатор соединения. Сайт никогда не получает URL или токен. Используйте HTTPS для подключения к Интернету или вашей надежной частной сети. Токены Gateway являются личными аккредитивами, не делятся ссылками.

Мост рабочего пространства и его инструменты MCP запускаются только для местоположения проекта → Этот экземпляр с удаленным агентом. Обычные сеансы не начинают ни одну из этих служб. Закрытие последней распределенной сессии останавливает частный прослушиватель моста; закрытие только его браузера или мобильного просмотра оставляет сессию.

Codex в настоящее время требует входа с поддержкой файла (auth.json) в B для этого режима; вход с помощью ключа явно отклонен. Его частная конфигурация использует ссылки на файлы врожденной учетной записи и беседы, не копируя значения OAuth. CLI совместимость и политики управления машиной все еще применяются.

Работа с удаленным агентом

Помощь в приложение и спросите что-либо

Откройте любую дверь на A, откройте Модель (чип настроек агента в композиторе), а затем выберите сохраненное соединение B в разделе Run на. Выберите Harness → Model → Effort → Permissions из каталога B. Вы также можете сохранить там соединение с помощью Добавить удаленный пример…. Оба случая требуют рекламы версии conversationWorkspace: 1; отсутствует область действия или запущены недоступные блоки B.

Для Prompt to app, опишите приложение и подтвердите его имена приложений/проектов, как обычно. Проект, сохраненное Приложение, надзор за разработкой и просмотр принадлежат A. Последующие сообщения, визуальные редактирования и выбранные области экрана переходят к агенту B. План сначала → Один агент может представить свою контрольную точку плана на A; Конвергенция в настоящее время требует Этот пример.

Для Задать вопрос, введите свой вопрос и любые файлы. Его частная папка беседы и Файлы инвентарь принадлежат A. Приложенные изображения и PDF-файлы можно прочитать через инструменты рабочего пространства. Конвертировать в проект копирует эту папку в обычный проект A.

Эти двери поддерживают обычную беседу о идентичности и истории. Открытие завершенного разговора и выбор Резюме явно возобновляет тот же контекст на B; воссоединение сети только воссоединяет читателей. Приложения, командный выход и частный транскрипт остаются доступными на A. Настройки агента могут изменять разрешения рабочего пространства А и модель/усилие Б. Терминал показывает B и является только для чтения; команды запускаются на A и появляются в чате. Браузер, планирование и инструменты Knowledge подвергаются воздействию на спрос через существующие возможности A разговора. Ни один помощник или мост не добавляется к обычному локальному разговору с помощью этой функции.

Чат показывает новые транскриптные события и диалоги разрешений/запросов. Отправьте последующие сообщения, ответьте на диалог или используйте Stop для прерывания текущего ответа. В режиме подключенного рабочего пространства разрешения выполняются A: режимы только для чтения отказывают в письмах и командах; нормальные режимы спрашивают в этой беседе; режимы редактирования-одобрения разрешают редактирование файлов, но запрашивают команды оболочки; Обход разрешает запрошенные операции. Отдельная форма удаленного рабочего пространства новой сессии фиксирует режим разрешений при запуске; две двери разговора выше раскрывают нормальный контроль разрешений А. Терминал агента (только для чтения) показывает фактический выход поставщика B; команды проекта запускаются на A через инструменты рабочего пространства и передают их выход в чате. В режиме удаленного проекта Terminal также принимает вход и перерасчет.

Терминал транспортирует живые выходы через WebSocket. Чат использует непрерывный SSE ответ и публикует события, когда провайдер пишет свой транскрипт; некоторые провайдеры записывают завершенное сообщение, а не каждый токен. Это не обещает обновления чата на уровне токенов для каждого CLI. Отдельный текст-только /chat/completions API все еще использует с буфером SSE.

Сетевое воссоединение читается с последнего курсора транскрипта. Он никогда не запускает другого агента или автоматически пересылает сообщение. После неопределенного запуска, перезапустите Новая сессия, выберите соединение и посмотрите в разделе Удаленные разговоры. Явное пересмотр одного и того же запроса сохраняет свое намерение id. Не меняйте идентификатор только для того, чтобы обойти неопределенный результат.

Закрытие вида отключает только его читателя. Удаление сохраненного соединения закрывает его доступ к рабочему пространству и останавливает его собственные команды на A; он сохраняет разговор B. Закрытая сессия останавливает агент и его принадлежащие команды; его транскрипт остается на удаленном экземпляре и может быть открыт для чтения. Начните новую сессию, чтобы снова запустить агента после его закрытия. Сохраненные удаленные разговоры перечислены под их соединением в новой сессии. В этом параграфе описывается отдельная форма удаленной сессии. Призыв к приложению и спросите что-либо, чтобы сохранить свою регулярную историю и явную акцию перезагрузки на A.

Сфера действия, отмена и ограничения

Токен для Приложение даёт доступ только к API ИИ для текста и изображений. Для Другой экземпляр настройка по умолчанию открывает рабочую область экземпляра, запускающего сеанс. Выбор Этот экземпляр и проекта при создании токена вместо этого разрешает сеансы только в этом проекте и доступ лишь к беседам, созданным с этим токеном. Он не предоставляет вход владельца, настройки, другие беседы, произвольные файловые API владельца или управление токенами. Инструменты агента работают с разрешениями, выбранными при запуске; выбор проекта не создаёт изоляцию на уровне ОС.

Доступ к подключённым рабочим областям по умолчанию включён в новых токенах для Другой экземпляр; к старым токенам он автоматически не добавляется. Каждое запуск на A связывает одну личность каталога проекта, соединение, режим разрешения и намерение. B не имеет API для изменения этого корня. Пути файлов являются относительными, внешние симлинки отклоняются, а письма требуют SHA-256, наблюдаемого при чтении файла. Неоднозначные или неоднозначные редакции отклоняются. У одобренных команд оболочки есть разрешения пользователя A; проект cwd не является песочницей ОС.

Эффекты рабочего пространства имеют устойчивые поступления. При воссоединении восстанавливается только та же операционная идентификация и возвращается ее записанный результат. Серверная авария во время эффекта сообщает о неопределенности, а не о ее перезагрузке. Владельческие команды останавливаются, когда сервер A выходит, когда доступ отменяется или когда сессия закрывается. Проверьте эффекты неопределенной команды, прежде чем запрашивать новую. Перезапуск любого из этих инстанций никогда не запускает другого провайдера или не запускает запрос.

Токены истекают после 90 дней. Отмена токена или отключение удаленного шлюза удаляет его сессионный авторитет и останавливает его агентов. Срок истечения проверяется при каждой операции и через ограниченную проверку фона, в том числе после перезагрузки сервера. Удаление сохраненного соединения не отменяет первоначальный токен: отменить его на выдавшем инстанции, если вы хотите отменить полномочия.

Существует максимум 20 активных сеансов шлюза на экземпляр эмитента и четыре живых читателя на токен, с 20 читателями по шлюзу. Каждый открытый чат использует один читатель; открытый терминал может добавить другой. Это ограничения местных ресурсов, а не подписки. Квоты поставщиков и требования к совместимости по-прежнему применяются; ни один поставщик CLI не устанавливается или обновляется путем подключения.

Подключенные рабочие пространства добавляют до 20 постоянных инструментальных каналов на B и четыре на токен, с четырьмя вызовами в полете и четырьмя запущенными командами проекта на рабочее пространство. A позволяет 20 активных рабочих пространств и 20 команд в целом. Фрейм ограничивается 2 МиБ, регулярные файлы/результаты редактирования – 8 МиБ, вызовы – 20 000 на рабочее пространство, а команды – 10 минут. Инструмент отвечает на содержимое файлов страницы и явно отмечает отрезанные командные хвосты. Страницы для чтения имеют максимальный объем 128 КБ; аргументы текста для написания/настройки имеют свои собственные границы схемы и должны помещаться в одну рамку. Это местные ресурсные ограничения; не создается дополнительного общественного происхождения.

Сессия API для личного бак-энда или инструмента AI

ТЭО сессии OpenAPI схема описывает этот протокол. Используйте Authorization: Bearer TOKEN при каждом вызове на /api/ai-gateway/v1 Base URL. Отказаться от перенаправлений; никогда не использовать URL/query credentials. Не помещайте токен в пометки, исходный код или хранилище браузера.

Конечная точка Значение
GET /session-catalog Версия протокола , опциональный авторизованный проект, workspaceAccessи каталог живого двигателя/модели/усилия/разрешения
POST /sessions Запустите врожденного агента; требуется UUID intention, engine, model, permissionMode, initialPrompt; опционально effort, name
GET /sessions До 100 недавних разговоров, созданных этим токеном
GET /sessions/{id} Текущее резюме разговора, принадлежащего этому токену
POST /sessions/{id}/resume Эксплицитное продолжение рабочего пространства разговора с UUID intention, initialPrompt, опциональный model/effort; тот же контекст B
POST /sessions/{id}/events Непрерывный SSE; JSON { "offset": null } первоначально, затем последний возвращённый отклонение на воссоединение
POST /sessions/{id}/transcript Читать названную страницу; отправлять offset для более новых событий или before для более ранних сообщений
POST /sessions/{id}/input Последующее: UUID intention и непустое text
POST /sessions/{id}/interaction UUID intention, текущий диалог idи поддерживаемый вариант/текст/отказ от ответа
POST /sessions/{id}/command UUID intention и action: interrupt, close, model, effort или permission; изменения выбора требуют arg из каталога
GET /sessions/{id}/terminal Автентифицированный WebSocket Upgrade; сырой вывод терминала, JSON вход { "t": "i", "d": "text" }, измененный размер { "t": "r", "c": 100, "r": 30 }
GET /workspaces/{id} Восстановить сессию B с помощью первоначального рабочего пространства UUID; никогда не создавайте замену на потерянный ответ
GET /workspaces/{id}/channel A открывает аутентифицированный WebSocket для B перед запуском; B отправляет вызовы инструментов рабочего пространства, A возвращает их результаты

Для встроенного моста A-workspace используйте автентифицированный владельцем API A от текущего клиента: сохраните B с POST /api/ai-gateway/connections, узнайте варианты с GET /api/engines?remoteConnectionId=ID&workspaceLocation=local, затем вызовите POST /api/ai-gateway/connections/ID/sessions с localProject: "P1" и выберите каталог. A обеспечивает идентификацию рабочего пространства и обратный канал. Вернутое имя agremote_… открывает /api/ai-gateway/remote-sessions/NAME/events и /interactionA, включая одобрения на A. Токен порта B не может авторизовать эти конечные точки владельца. Следующий пример показывает это полное различие.

Для Промпт к приложению и Задай что-либо, используйте обычный аутентифицированный владельцем POST /api/sessions на A с выбором каталога remoteConnectionId: "SAVED_CONNECTION_ID" и B. Используйте askAnything: true для вопроса или project и createMode: true для существующего проекта приложения. Создание нового приложения сначала использует POST /api/create-mode/projects/confirmed с prompt, appName и projectName. Задерживайте idempotencyKey при неопределенном запуске. Ответ имеет обычные метаданные A tmuxName, sessionId и remoteAgent . Следование, транскрипт, рабочее пространство и резюме используют обычные API разговора А; никогда не заменяйте токен шлюза для аккредитации владельца А.

Если вы реализуете отдельный сервер соединения, обратитесь к протоколу рабочего пространства и схемам инструментов. Запустите на B с workspace: { id: UUID, project: "P1" } только после реализации канала, прикрепленного локального корня, разрешений и долговременных операционных квитанций. Сама по себе этикетка не предоставляет доступ к файлам. Вход терминала B и изменения врожденных разрешений отключены для этих сессий.

В SSE update содержится { id, project, session, transcript }. transcript.events является новой страницей событий, offset является ее непрозрачным целым курсором, а interaction или terminalAttention описывает текущие потребности в вводе. Поддерживайте полные рамки SSE в сетевых блоках. Не игнорируйте комментарии к Защитнику. На событии error сохраните разговор и восстановите связь для чтения. Никогда не следует делать вывод о том, что операция не произошла из-за потерянного ответа сети.

Режимы разрешений, идентификаторы моделей и усилия поступают от /session-catalog, а не от текстовых идентификаторов композитных моделей API. Создание сеанса и действия отклоняют неподдержанные поля. Первоначальные запросы и последующие записи ограничены 64 КБ. Намерения запуска охватываются токеном; намерения действий охватываются сессией. Повторное использование намерения с разным содержанием отклоняется. После того, как была предпринята попытка доставки времени запуска, неизвестный результат остается AI_GATEWAY_SESSION_UNCERTAIN и не может быть воспроизведен автоматически.

Для стены квоты подождите фактическое восстановление поставщика; неизвестное время перезагрузки остается неизвестным. Для AI_GATEWAY_SESSION_ATTENTIONответьте на текущий диалог. Для отозванного/утерянного доступа создайте новый токен с расширенным диапазоном на инстанции-эмитенте. В случае отсутствия моделей, обновите каталог. Существующие разговоры владельца не могут быть приняты с новым токеном.

Попросите приложение и спросите что-либо на A

Тот же собственник API сохраняет файлы, историю и просмотр на A, в то время как сохраненное соединение B запускает агента.

// Node.js 22+. Private backend only; never send these credentials to the website.
const origin = new URL(process.env.AIRCODE_LOCAL_ORIGIN);
const ownerToken = process.env.AIRCODE_LOCAL_AUTH_TOKEN;
const connectionId = process.env.AIRCODE_REMOTE_CONNECTION_ID;
const idempotencyKey = process.env.AIRCODE_REMOTE_INTENTION;
if (!['https:', 'http:'].includes(origin.protocol) || origin.username || origin.password
    || origin.pathname !== '/' || origin.search || origin.hash
    || !ownerToken || !connectionId || !idempotencyKey) throw new Error('Configure A, its saved B connection and one retained intention UUID');
async function call(path, body) {
  const response = await fetch(origin.origin + '/api' + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error',
    headers: { authorization: 'Bearer ' + ownerToken, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('A refused the operation: HTTP ' + response.status);
  return response.json();
}
const catalog = await call('/engines?remoteConnectionId=' + encodeURIComponent(connectionId) + '&workspaceLocation=local');
const engine = process.env.AIRCODE_REMOTE_ENGINE, model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from B’s catalogue');
}
const selection = { remoteConnectionId: connectionId, engine, model, permissionMode,
  ...(process.env.AIRCODE_REMOTE_EFFORT ? { effort: process.env.AIRCODE_REMOTE_EFFORT } : {}) };
// Set AIRCODE_APP_PROJECT only for an existing A application project.
// To create one first, POST /create-mode/projects/confirmed with confirmed
// { projectName, appName, prompt }, then retain its returned name.
const project = process.env.AIRCODE_APP_PROJECT;
const session = await call('/sessions', { ...selection, idempotencyKey,
  ...(project ? { project, createMode: true } : { askAnything: true }),
  initialPrompt: process.env.AIRCODE_PROMPT || 'Explain how this distributed conversation works.',
});
console.log('Conversation on A:', session.tmuxName, 'Agent:', session.remoteAgent.instanceName);
// Read-only observation; never replay the launch or prompt to reconnect.
const transcript = await call('/sessions/' + encodeURIComponent(session.tmuxName) + '/transcript');
for (const event of transcript.events) if (event.text) console.log(event.text);
// Poll the same route with ?offset=LAST_OFFSET for incremental updates.
// Send an explicit follow-up to POST /sessions/NAME/input with { text }.
// Resume explicitly with POST /sessions { project: session.project,
//   resumeSessionId: session.sessionId, initialPrompt: 'Continue' }.

Проект на А, агент на Б

Используйте аккредитацию сеанса владельца A в частном бак-энде. Токен шлюза B уже сохранен на A и не может заменить этот аккредитив владельца. Выберите значения каталога и сохраните UUID намерения запуска.

import { randomUUID } from 'node:crypto';

// A owns P1. B supplies the agent/account. Never send these values to the website.
// Save B once in A's Settings → AI gateway → Remote instances.
const origin = process.env.AIRCODE_LOCAL_ORIGIN;
const ownerToken = process.env.AIRCODE_LOCAL_AUTH_TOKEN;
const connectionId = process.env.AIRCODE_REMOTE_CONNECTION_ID;
if (!origin || !ownerToken || !connectionId) throw new Error('Configure A and its saved connection to B');
const url = new URL(origin);
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password
    || url.search || url.hash || url.pathname !== '/') throw new Error('Use A’s instance origin');
async function call(path, body, signal) {
  const response = await fetch(url.origin + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error', signal,
    headers: { authorization: 'Bearer ' + ownerToken, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('A refused the operation: HTTP ' + response.status);
  return response;
}
const id = encodeURIComponent(connectionId);
const catalog = await (await call('/api/engines?remoteConnectionId=' + id + '&workspaceLocation=local')).json();
const engine = process.env.AIRCODE_REMOTE_ENGINE;
const model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from B’s catalogue');
}
// Persist the intention before launch and reuse it if its response is lost.
const intention = process.env.AIRCODE_REMOTE_INTENTION;
if (!intention) throw new Error('Set AIRCODE_REMOTE_INTENTION to a new UUID once, then retain it');
const session = await (await call('/api/ai-gateway/connections/' + id + '/sessions', {
  intention, localProject: process.env.AIRCODE_LOCAL_PROJECT || 'P1',
  engine, model, permissionMode, effort: process.env.AIRCODE_REMOTE_EFFORT || '',
  initialPrompt: 'Read the project instructions, then explain which machine owns its files.',
})).json();
console.log('Conversation on A:', session.tmuxName);
const path = '/api/ai-gateway/remote-sessions/' + encodeURIComponent(session.tmuxName);
// Approvals from A appear as transcript.interaction with id "workspace:…".
// Only after the owner chooses an answer, send it to A:
async function answerCurrentInteraction(interactionId, option) {
  return call(path + '/interaction', { intention: randomUUID(), id: interactionId, option });
}
// This bounded reader displays complete events and pending owner decisions.
// It never approves an operation automatically.
const signal = AbortSignal.timeout(30_000);
const stream = await call(path + '/events', { offset: null }, signal);
const decoder = new TextDecoder(); let buffer = '';
try {
  for await (const chunk of stream.body) {
    buffer += decoder.decode(chunk, { stream: true }).replace(/\r\n/g, '\n');
    let boundary;
    while ((boundary = buffer.indexOf('\n\n')) >= 0) {
      const frame = buffer.slice(0, boundary); buffer = buffer.slice(boundary + 2);
      const data = frame.split('\n').filter(line => line.startsWith('data:'))
        .map(line => line.slice(5).trimStart()).join('\n');
      if (!data) continue;
      const update = JSON.parse(data);
      console.log(JSON.stringify(update));
      // Display update.transcript.interaction and let the owner choose an answer.
    }
  }
} catch (error) { if (!signal.aborted) throw error; }
// Disconnecting the reader leaves the agent and workspace connection running.
// An explicit POST path + '/command' with {intention:UUID,action:'close'} stops them.

Живая сессия на проекте B

Node.js 22 или более поздний. Настройте AIRCODE_REMOTE_BASE_URL и AIRCODE_REMOTE_TOKEN в конфигурации частного сервера. Первый запуск печатает каталог до тех пор, пока вы не выберете также AIRCODE_REMOTE_ENGINE, AIRCODE_REMOTE_MODEL и AIRCODE_REMOTE_PERMISSION. AIRCODE_REMOTE_EFFORT является дополнительной опцией. Читатель закрывается через 30 секунд; удаленный агент продолжает работать.

import { randomUUID } from 'node:crypto';

// Node.js 22+. Configure these privately on your application's server.
const base = process.env.AIRCODE_REMOTE_BASE_URL;
const token = process.env.AIRCODE_REMOTE_TOKEN;
if (!base || !token) throw new Error('Configure the remote Base URL and token');
const url = new URL(base);
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password
    || url.search || url.hash || url.pathname !== '/api/ai-gateway/v1') {
  throw new Error('Use the instance Base URL without a trailing slash');
}
async function call(path, body, signal) {
  const response = await fetch(base + path, {
    method: body === undefined ? 'GET' : 'POST', redirect: 'error', signal,
    headers: { authorization: 'Bearer ' + token, 'content-type': 'application/json' },
    ...(body === undefined ? {} : { body: JSON.stringify(body) }),
  });
  if (!response.ok) throw new Error('Remote request refused: HTTP ' + response.status);
  return response;
}
const catalog = await (await call('/session-catalog')).json();
const engine = process.env.AIRCODE_REMOTE_ENGINE;
const model = process.env.AIRCODE_REMOTE_MODEL;
const permissionMode = process.env.AIRCODE_REMOTE_PERMISSION;
if (!engine || model === undefined || !permissionMode) {
  console.log(JSON.stringify(catalog, null, 2));
  throw new Error('Choose harness, model and permissions from this catalogue');
}
// Keep this intention for an explicit retry of this exact launch request.
const intention = process.env.AIRCODE_REMOTE_INTENTION || randomUUID();
const session = await (await call('/sessions', {
  intention, engine, model, permissionMode,
  effort: process.env.AIRCODE_REMOTE_EFFORT || '',
  initialPrompt: 'Describe the project without changing its files.',
})).json();
console.log('Remote conversation:', session.id);
// Demonstrate a bounded live reader. Disconnecting leaves the agent running.
const signal = AbortSignal.timeout(30_000);
const stream = await call('/sessions/' + session.id + '/events', { offset: null }, signal);
const decoder = new TextDecoder();
try {
  for await (const chunk of stream.body) process.stdout.write(decoder.decode(chunk, { stream: true }));
} catch (error) { if (!signal.aborted) throw error; }