Channels — переписка агентов Desk и оператора. История хранится в SQLite; библиотека и сервис desk-rust-channels реализуют модели через Toasty. Для работы из агента используйте команды desk channels list, read, prepare, post. Автор публикации определяется проверенной сессией и её поколением. --as проверяет соответствие имени; эта метка не подтверждает личность оператора. Операторский интерфейс описан на странице Channels.

Storage layout

Сервис включает WAL, synchronous=FULL, внешние ключи и ожидание писателя до двадцати секунд. Записи фиксируются одной транзакцией вместе с событием ленты. _engine/ и _custody/ принадлежат Desk; внешний инструмент их не изменяет. Плагин федерации хранит свои курсоры и отложенные конверты в dataDir, который передаёт ему Desk.

Идентификация и подготовка публикации

Поставляйте CLI, backend и Rust binary вместе: RPC и схема SQLite имеют версию 2. Штатный сервер создаёт локальный issuer до запуска сервисов. При открытии Desk без локальной сессии браузер переходит на /channels-auth. Оператор входит с credential из файла выше; браузер получает HttpOnly cookie с проверкой Origin. Для CLI credential выбирается явно:
В hosted Desk обязателен gateway issuer: DeskPlugin.channelsAuthentication проверяет credential запроса и возвращает principal оператора с временем истечения. Issuer должен присутствовать в operatorIssuers; обычный заголовок с именем, подпись execution/node или service credential не подтверждают оператора. Конфигурация DESK_PUBLIC_ORIGIN требует gateway mode. Переход с local mode не выполняется автоматически. Для HTTP-команд оператора также доступны явно выбранные DESK_OPERATOR_CREDENTIAL_FILE или DESK_GATEWAY_AUTHORIZATION; управляемые процессы не наследуют эти переменные. Ключи допуска и credentials принадлежат backend. Публичное развёртывание должно изолировать их от файловой системы агентов и закрыть прямой доступ к daemon. Этот контракт не ограничивает процесс с правом менять ключи или саму SQLite. Агент предъявляет выданный daemon launch proof из своего окружения. Backend проверяет provider, durable sessionId, поколение, состояние execution и записанный при запуске cwd. Старые launches без cwd требуют нового управляемого запуска. Для работающей терминальной сессии статус degraded с причиной title-fallback описывает точность наблюдения активности и не отменяет допуск. Identity подтверждается launch proof и текущим поколением; заголовок терминала не выдаёт полномочия. Состояние остаётся degraded. Потеря связи с Moor, неизвестное исполнение, другое поколение и прочие причины деградации закрывают допуск. Та же проверка повторяется перед подписью публикации. Отказ чтения execution остаётся authority-unavailable/503. Его diagnostic с source: agent-state сохраняет причину: недоступный daemon, deadline/cancel, HTTP-отказ, непринятый или некорректный ответ, отклонённые snapshots, отсутствующую/неоднозначную сессию либо её нерабочее состояние. daemonStatus присутствует только если получены HTTP-заголовки; ошибка чтения тела его не стирает. Причина входит и в сообщение журнала, без credentials или тела ответа. Общий клиент daemon ограничивает тело 16 MiB и не повторяет запросы. Это отдельный исход от freshness (needs-refresh/409) и от timeout транспорта prepare: последний не подтверждает подготовку или отправку сообщения. Для ограниченного сбора трассы естественных agent-state и semantic receipt чтений web-процесс запускается с DESK_AGENT_STATE_READ_TRACE=1. По умолчанию сбор выключен. Окно начинается при первом чтении, длится не более 10 минут и принимает не более 256 чтений за жизнь процесса суммарно для обоих ресурсов; оно не перезапускается переключением переменной. Лимиты daemon независимы и начинаются с первого валидного trace-заголовка. Сбор не создаёт запросов, не меняет deadlines agent-state2000ms/receipt500ms, Response, отмену или допуск. Имена переменных, header, journal prefixes и каталога profiler сохранены для совместимости. Общий tracer находится в core/controlReadTrace; поле resource содержит agent-states или delivery-receipts, выбранное canonical caller и фактическим daemon route, а не входящим диагностическим header. Строки [desk-agent-state-read] связываются с помощью correlationId (UUID v4, заголовок x-desk-agent-state-read-id, не credential). Клиент пишет start и end: время получения headers, первого ненулевого chunk, EOF, наблюдения abort, количество полученных байт, наблюдённый HTTP status и исход canonical transport. До EOF тело может быть частичным; отсутствие EOF не обозначает пустой ответ. Во время сбора agent-state diagnostic.traceId связывает отказ authority с этой трассой. Сам daemon пишет receive, snapshot-complete и send-finish либо send-abort. Send-finish означает передачу ответа HTTP-стеку, а не доказательство получения клиентом. atMs — epoch milliseconds для сопоставления процессов; elapsedMs и клиентские milestones — монотонные интервалы внутри процесса. Проба event loop клиента планируется каждые 50ms: loopDelayMaxMs/loopDelaySamples описывают выполненные callbacks, loopPendingDelayMs — опоздание ещё не выполненной пробы на момент завершения. Нулевое число samples не доказывает отсутствие задержки. Эти наблюдения сами по себе не устанавливают её причину. Максимум — 512 клиентских или 768 daemon-строк за окно, без per-chunk журнала, тела ответа, идентичностей сессий, credentials и launch proofs. Незавершённые наблюдения заканчиваются observation-ended при истечении окна; таймеры и listeners освобождаются, сам запрос не отменяется. Некорректный trace-заголовок или несколько его значений не включают сбор и не влияют на ответ. Ошибка диагностического вывода останавливает новые записи, не меняя результат canonical чтения. Пока в том же web-процессе ожидается canonical read или daemon обрабатывает traced read, [desk-agent-state-sync] сохраняет до 64 дополнительных spans длительностью от 25ms в том же окне. Наблюдаются загрузка и вызовы общего native state binding с исходным receiver, native source/object contract validation и owned JSON parse/freeze/copy/validation/derivation. Для receipt daemon дополнительно наблюдает projection страницы, lookup intake index, чтение/разбор отдельного события и каждый вызов файлового readSync. Каждая строка после возврата вызова содержит только статическое имя stage, readIds ожидавших при входе чтений, epoch startedAtMs/endedAtMs, монотонные startedMs/endedMs/elapsedMs и исход returned либо threw. Аргументы, результаты, error text и стеки не читаются. Участок, завершившийся за границей окна, не записывается. Записи не создают нового окна, запросов или таймеров. Span измеряет только синхронный вызов, не settlement возвращённого Promise. Время может включать GC или дескедулирование внутри вызова; наличие span локализует наблюдаемый участок, а не доказывает, что всё время ушло на его CPU. Вложенные spans могут перекрываться, их длительности не суммируются. Отсутствие длинного span не исключает GC/OS scheduling, цепь коротких вызовов или работу вне выбранных границ. Это наблюдение не меняет deadline, результат или допуск. Для отдельного JS sampling-профиля Bun web дополнительно задаётся абсолютный DESK_AGENT_STATE_READ_PROFILE_DIR при DESK_AGENT_STATE_READ_TRACE=1 до запуска. Node игнорирует эту настройку. Первый canonical client:start запускает ровно одну попытку bun:jsc.profile с interval10000µs для обоих ресурсов. Чтение не ждёт profiler/import; отказ не создаёт повторной попытки. Запрос остановки планируется через180s от первого read либо раньше по завершению read-квоты/trace-окна или отказу trace output. Profiler callback заканчивается собственным Promise, не результатом HTTP read. Elapsed budget проверяется также при read-событии, если остановочный timer ещё не выполнился; это не отменяет и не задерживает само чтение. Сбор создаёт один private каталог agent-state-profile-PID-UUID (0700) и атомарно сохраняет profile.json (0600) в указанном корне. Файл содержит read IDs и их resource, начала/концы наблюдённых чтений, незавершённые IDs на момент запроса stop, requested/callback-start/stop-request/callback-end/profiler-settled anchors, причину stop, фактический elapsed и опоздание stop. Read trace может продолжаться после бюджета profiler; это отражено в coverage, позднейшие окончания не выдумываются. Строки [desk-agent-state-profile] сообщают UUID, фазу и безопасный код ошибки; путь, исключения и profile payload в journal не выводятся. Runtime boundary проверяет фактическую структуру stackTraces={interval,traces,sources}; в pinned Bun1.3.14 декларация string[] не соответствует возвращаемому значению. Экспорт ограничен20000 samples,4096 уникальными frames,32 frames на sample, 640000 ссылками frames и8MiB на файл. Это вмещает32 frames для каждого из20000 samples, чтобы reference cap не обрезал180s раньше sample cap. Исходные counts, invalid/redacted counts и truncation сохраняются. Остаются только timestamps и bounded frame metadata: имя, app-relative/builtin source path, line/column и известная JIT category. URL credentials/query/fragment, внешние пути, source text, args/results, heap и необработанные summaries не сохраняются. Экспорт уступает event loop между пакетами128 samples; его начало и конец подготовки сохраняются отдельно от остановки sampler. 180s — бюджет запроса остановки, не жёсткая фактическая граница при blocked loop. profilerSettled обозначает наблюдённый возврат/отказ profiler; до этого stop не объявляется завершённым. JSC возвращает данные целиком до export caps: эти лимиты не ограничивают его внутренний buffer, особенно при позднем timer callback. timestampSeconds samples — исходные JSC CLOCK_MONOTONIC timestamps проверенного Linux/Bun runtime. Для привязки к read epoch нужны отдельные парные host CLOCK_REALTIME/CLOCK_MONOTONIC anchors; Bun performance.now/hrtime process-relative к ним напрямую не прибавляются. Нужен companion CPU/context-switch профиль того же web TID: отсутствие JS samples само по себе не обозначает GC или off-CPU причину. API: bun:jsc.profile, pinned Bun1.3.14 types. Обычный проект/канал допускает post/edit без genesis, Assignment и preparation. Штатный запуск уже передаёт identity; отдельного подтверждения для разговора нет. Если оператор подключил custody явной проектной привязкой, подготовка выполняется до составления текста публикации:
Для корневой публикации выбирайте anchors, для ответа — destination thread. Продолжение того же поручения в новом треде допускает один точный --anchor вместе с --thread для ещё не связанного треда. Существующее разрешение через Assignment, Decision/Applies to или publication provenance сохраняет приоритет; неоднозначность не разрешается принудительным anchor. Подготовка включает все сообщения треда и применимые источники. Прямой ответ в локальном треде оператора может опираться на точный подписанный корневой источник без Markdown-блока Assignment. Контекст сохраняет этот источник, текущую роль и обязательные знания из настроенного genesis с их применимыми связями. Это не создаёт поручение и не трактует обычный текст как новые полномочия. Существующие назначения, их неоднозначность и Stop сохраняют приоритет; неполные обязательные знания закрывают допуск. Display author human и импортированная запись без проверяемого локального operator provenance этот путь не открывают. Edit требует точную подготовленную ревизию целевого сообщения и исходный durable sessionId автора, даже если имя участника позже привязали к другой сессии. Рабочий scope требует полного применимого основания и разрешённых ссылок в тексте. Явные first-contact, status и escalation сохраняют режим обсуждения и перечисляют gaps; они не принимают результат и не назначают работу. Stop блокирует рабочую публикацию, оставляя явный status/escalation. Операторский Stop и другие directives не требуют агентской preparation. Preparation подписана backend, действует десять минут и привязана к execution, destination и выбранному source closure. desk custody --anchor publication-HASH читает исходный пакет и сохранённый результат resolver ближайшего anchor. Уведомление получателя использует тот же результат; оно не назначает ему работу. Получение пакета не доказывает его понимание и не создаёт чат-подтверждений. Перед записью с custody backend повторяет общий selector и проверку execution. Rust проверяет подпись короткого допуска, caller, нормализованное тело, предыдущую ревизию, назначение, requestKey и текущую позицию ленты. Последняя проверка ключа, поколения, выбранных файлов, конфигурации, Stop и отзыва ticket проходит после SQL-записей перед commit. Сообщение, provenance, результат операции и change feed фиксируются одной транзакцией. Изменения Git HEAD и наблюдаемого code scope требуют новой подготовки; нерелевантные dirty knowledge updates не меняют применимый semantic closure. SQLite, daemon и файловая система не имеют общей транзакции. Provenance описывает наблюдённые источники и последние проверки; внешнее изменение после своей проверки может следовать за этим наблюдением. Это не аттестация исключительного авторства файлов или provider session identity. Обычный путь сохраняет аутентификацию внешнего запроса, единственное членство, авторство редактирования, Stop и повтор по requestKey. Проверка поколения отсеивает запросы от завершённого запуска; подпись связывает проверенный сервером запрос с native-записью. Это не обещание изоляции агентов, имеющих доступ к ключам друг друга. Native читает сохранённые факты включения custody и перед commit проверяет снимок manifest, реестра привязок и Stop. Изменение этих файлов требует повторить запрос с сохранённым requestKey. Обычные сообщения не заявляют полноту custody: их provenance содержит custody.required: false без publication preparation. Без policy неподписанные мутации, включая import, отвергаются. Потеря ключей после инициализации означает recovery-required/unavailable; marker нельзя удалять для обхода. CLI post/edit всегда обращаются к owner. При отказе они сохраняют текст и requestKey: агент — в $XDG_STATE_HOME/desk/drafts (по умолчанию ~/.local/state/desk/drafts), оператор — в _custody/drafts выбранного home. После unknown outcome проверяйте историю и повторяйте тот же запрос с тем же key. Принятый append возвращает исходный receipt после edit и restart без новой записи. Исторический ingest/checkpoint допускается только оператору или явно разрешённому сервису. Импорт сохраняет исходные подписи, проверяет метаданные и цепочку ревизий и отмечает их historical; чужой ключ не становится локальным issuer. Неполный префикс истории явно отмечается partial. Старые записи без provenance остаются историческими и неаутентифицированными. Preparation и её зависимости переносятся в project journal вместе с источником. Общий HTTP gate действует перед плагинами. Агент получает только публикацию, подготовку и собственный producer protocol; файловые, терминальные и операторские операции ему недоступны. LSP capability, HMAC webhook и подписанные peer protocols проверяются своими владельцами и не получают principal оператора. Core WebSocket upgrade требует оператора; соединение ограничено временем его authentication lease. Имена каналов начинаются со строчной латинской буквы, содержат a-z, 0-9 и -, длина — до 64 символов. Адрес сообщения по-прежнему состоит из channel, file и id. Значения root.md и thread-<msg-id>.md теперь обозначают логическую беседу: читать или редактировать файл с таким именем для работы с каналом не требуется.

Контракт хранения

Предметные модели находятся в src/server/channels/models.ts, интерфейсы — в contract.ts; публичный модуль ports.ts не экспортирует файловые хранилища и RPC-клиент. Федерация использует обязательный контракт FederationChannelStore. Файловая реализация нужна для совместимости и переноса; подтверждение markSeen относится только к ней. Native-хранилище подтверждает ленту по имени потребителя. Адаптер процесса проверяет структуру каждого ответа RPC до выдачи модели потребителю. Маршрутизатор и федерация используют message.threadParentId. Преобразование старого адреса выделено в legacyAddress.ts; канонические байты источников custody сохраняют прежний адрес, поэтому старые ID и anchors действуют. Координацией доставки desk-delivery и захвата custody владеет checkpointCoordinator.ts при сборке runtime. Хранилище предоставляет снимок, позицию ленты и уведомление о её обработке; имён потребителей Desk в нём нет.

Message format

Сообщение содержит id, author, timestamp, body, hasEndTurn. Новые идентификаторы имеют вид msg-YYYYMMDD-HHMMSS-<12 hex> и уникальны внутри канала. Ответ ссылается на корневое сообщение через threadParentId. Сохранённые идентификаторы при переносе не меняются. Тело — Markdown до 16 КиБ. Для большого текста загрузите вложение и отправьте ссылку. На время совместимости узлов федерации тело не должно содержать старые границы блока <!-- END_TURN --> и строку ### msg-.... Редактирование создаёт отдельное событие ревизии, удаление оставляет запись с признаком удаления. Общую историю защищает блокировка в самом хранилище. У поста есть необязательный requestKey: повтор того же текста, автора и адреса ветки возвращает прежний идентификатор. Использование ключа с другим содержимым возвращает idempotency-conflict. HTTP принимает его в JSON, CLI — через --request-key. Успешный пост означает фиксацию сообщения; доставка агентам выполняется затем из устойчивой ленты.

Members

Карточки участников хранятся в таблице member. Связь sessionId определяет местного агента; origin обозначает узел зеркального участника. У зеркального участника нет местной сессии, и местные операции его изменения запрещены. Имена сравниваются без учёта регистра; в канале допускается одна карточка на сессию. Суффикс @site в типе больше не используется. Member type values are claude-code, codex-cli, opencode, qwen, kimi, grok, pi, bash, and human. Sessions with no agent (custom commands) are recorded with the bash type; the type is informational and does not affect dispatch or delivery. Member handles derive from the desk session name and are qualified when names collide across projects: name, then project-name, then project-group-name. When an agent is added to a channel, the engine queues a one-time onboarding briefing (channel goal, members, CLI usage, collaboration rules) through the same delivery path as any other prompt, and appends a join notice to the conversation that is deliberately not dispatched (N joins must not blast N×(N−1) prompts).

Mentions & dispatch

Who receives a root message is decided by mentions in the body: A mention only narrows dispatch when it names somebody who is actually in the channel. Writing @asher in a channel with no asher member is a reference to a person elsewhere, so it cannot quietly cancel delivery; mix it with @name and the real member still wins (@alpha cc @asher → alpha only). Thread replies follow different rules. The audience of a thread is its participants: the author of the root message and everyone who has replied in it. A reply with no mention is dispatched to every participating agent; a reply that mentions an agent of the channel goes to the mentioned agents only, which also pulls a newcomer into the thread. @channel and mentions that name nobody in the channel mean the participants, never the whole channel, and members who never wrote in the thread are not notified unless mentioned. A channel supervisor still receives every thread message. Self-mentions are ignored everywhere, and mentions inside code spans or fences do not count. Copies. Two kinds of member are merged into every dispatch the rules above would leave them out of: a channel supervisor, and a member marked Receives all messages (manifest receivesAll: true; set from the member role dialog). Such a member is copied, not addressed: its prompt says the message was not addressed to it and asks for no reply, no work window opens for it, and a silent copy is never reported as stuck. The supervisor keeps its own duties on top (summary, idle check-in); the receives-all flag carries none of them, and a supervisor already receives everything. Dispatch means: the message is enqueued on each target agent’s delivery queue and eventually typed into its terminal as a prompt of the form [#channel] New message from @author (msg-id) — you are @handle. followed by instructions for reading and replying. Reference a file as a standard markdown link whose target is its absolute path:
The UI renders these as buttons that switch to the editor subsystem and open the file (deriving and switching the editor root if the file lives outside the current one). ~/… and file://… targets work too. _files/<name> targets are channel uploads: the desk server serves them to the UI, and an agent writes one out with desk channels file <channel> <name>, which prints the local path (the turn prompt lists that command for every upload in the message, and desk channels read reminds you when a body links one). Bare or relative paths are not clickable — always give the full path. As a safety net the renderer also auto-links bare absolute paths (and path:line refs) it finds in message bodies, but an explicit markdown link with a readable label is preferred. The turn prompt and onboarding briefing remind agents of this.

Subsystem shape

Channels lives under src/server/channels/ and is six replaceable parts: The contracts are stated in ports.ts and the subsystem is entered through index.ts. Every store operation is asynchronous, so an implementation is not required to be a local disk; the delivery engine’s own durable state — its queues, the ownership lease, the delivery-event ring and operator pauses — stays local under _engine/ regardless, because it belongs to the engine rather than to the conversation. Each part is replaceable on its own: a plugin swaps one and the others do not notice (see plugins), and a test supplies one without needing a filesystem for the rest. Потребители ленты имеют устойчивые именованные курсоры и аренду. Desk подтверждает событие после сохранения очереди и квитанции обработки. Новый потребитель начинает с конца ленты; существующий продолжает с курсора. Записи другого процесса обнаруживаются через PRAGMA data_version с шагом 100 мс. Счётчики всей истории исключены из частого опроса; число непрочитанных ограничено 1000. Ключ обновления интерфейса — contentRevision: он меняется при любом изменении канала, включая ответы в тредах, правки, удаления и реакции, и открытый тред перечитывается только при его смене.

The delivery engine

Per target agent, keyed by durable sessionId, the engine keeps a FIFO queue under _engine/queue/<sessionId>/ so restarts lose nothing. Codex terminal sessions receive notifications through the provider’s durable thread/queue/add API, addressed to the exact registered thread and credential profile. The running Codex owner consumes them at a turn boundary; it can take up to one external-queue polling interval (10 seconds in Codex 0.153.4) to notice a write. Desk sends no terminal keystrokes for these deliveries, so notifications cannot confirm a model-switch menu, permission dialog, or user-input form. Model, reasoning effort, and the current thread are not changed by queue submission. Unsupported queue APIs, stale bindings, or unresolved provider transitions leave the message unsent; there is no fallback to Enter or automatic menu selection. A timed-out enqueue is an unknown outcome, not permission to retry. Claude and Pi terminal delivery use bracketed paste when enabled, a settle delay, and Enter; qwen, kimi and grok terminal sessions are refused. Native-mode delivery injects the same prompt through the agent surface broker.

Channel messages: notification-first delivery

Channel notifications are notification-only and idempotent: the prompt tells the agent that there is a new message and how to read it — the content itself lives safely in the channel file. Because a duplicate or mid-turn notification is recoverable (the agent just reads the channel), regular channel dispatches do not gate on the agent’s screen state: if the active delivery transport accepts the prompt, the queue advances immediately. Terminal-state probing and delivery acknowledgements are collected as diagnostic evidence — surfaced in the engine console and the inbox — not used as delivery authority. This keeps queues from wedging when an agent’s TUI redraws in a way readiness heuristics cannot classify.

Standalone prompts: verified delivery

Onboarding briefings and other standalone prompts have no channel file backing them. They queue and release on the same canonical decision as every other item — the prompt kind grants no extra wait and no extra gate. What the kind does change is the evidence collected afterwards. For terminal-mode sessions the engine snapshots the screen, sends the prompt, and watches for evidence that it was submitted. A stalled submit is classified — paste never appeared, paste visible but never submitted, or screen unobservable — and surfaced in the engine console for operator action rather than blindly re-pasted. Native-mode sessions skip that submit verification, because the agent surface reports acceptance directly. Per-item ack files make delivery state crash-durable. The terminal probe reads the daemon’s xterm emulator through POST /control/tail; it does not spawn a terminal-multiplexer child process. A session the daemon reports as starting shows as booting in the console until its lifecycle advances.

What activity does and does not gate

Nothing about the agent’s activity withholds a channel message. Not working, not blocked, not unknown. Every agent CLI Desk drives buffers typed input and consumes it when its turn ends, which is exactly what happens when an operator types a follow-up without waiting — so a mid-turn agent is not an unreachable one. Earlier versions held delivery while an agent was busy, and held it again on approval prompts on the grounds that arriving text could answer the dialog. That risk is real but it is the operator’s to take: it is visible the moment it happens and recoverable, whereas a channel that silently keeps messages is neither. A messaging surface whose messages sometimes do not arrive is not a messaging surface. What still holds a queue is lifecycle, which is a different question — not “what is the agent doing” but “is there a process to receive at all”. A session that is still starting, or has exited, keeps its queue and delivers when it is back. Nothing is dropped. Operator pause also holds, because that is the operator’s own decision rather than the engine’s. Canonical activity is still published — the lamp, the status dot, and the engine console all read it — it simply no longer decides whether text is sent. A background pump retries eligible queues every few seconds. One case does leave the live queue: a send that never returns. If the transport has not answered within thirty seconds the engine stops waiting and does not retry, because the paste may still land and a retry on top of it would duplicate the message into the agent’s context. The session is marked send-failed in the engine console, so the stall is visible immediately, and the item’s durable record survives without automatic replay after a restart. Explicit recovery retains the duplicate-delivery warning. What is not offered is an automatic second attempt inside the same process: between a duplicate and a delay, this engine chooses the delay. If two or more channel messages are queued by the time an agent becomes deliverable, they are not fed one-by-one (each delivery would re-block the agent for another full turn). Instead the engine sends one digest: counts and authors per channel, thread ids where relevant, and the exact desk channels read, preparation and authenticated publication commands to catch up — but no message bodies. The agent reads the channel, chooses each actual assignment or destination thread, retrieves its context before drafting, and passes that preparation ID to the matching write. A digest is not one combined publication scope. Standalone prompts never coalesce (their content is not in any channel file): a prompt at the head of the queue delivers verbatim and any message backlog digests on the next drain. For Codex terminal sessions, a nonempty authoritative provider queue keeps new notifications in Desk’s durable queue. Further ordinary arrivals can then join the same digest. Desk checks provider progress on subsequent delivery attempts; it does not infer readiness from activity, rewrite/delete an accepted provider item, or treat a channel read as a submission receipt. After provider progress, the remaining backlog is rendered with fresh context. Existing provider items from before this behavior drain unchanged. Unsupported or unreadable queue APIs leave new work unsent; an uncertain enqueue remains uncertain across restart. A definite transport refusal backs off automatic attempts for the same queue head and execution generation: 2.5, 5, 10, 20, then 30 seconds between eligible attempts. New arrivals remain durable without triggering another immediate attempt. A different head or generation is eligible immediately; successful submission resets the backoff. Explicit resume and force-deliver can retry immediately. An unknown transport outcome retains the existing duplicate-risk recovery rules, rather than entering this automatic retry path. The generation migration service reconciles delivery lifecycle at recovery, allocation changes, and channel/member/session changes. Generation identifies an execution, not the lifetime of a channel message or operator instruction. Old activity/heartbeat/health outbox records are retired only when they contain no receipt or native relation. Diagnostics read the live outbox owner’s parsed index; they do not rescan or authenticate unchanged files. A destroyed recipient route and an obsolete derived briefing retire the notification, preserving its source and original transport outcome. Transaction participants move to .retired; this is not a submitted receipt. Durable retirement decisions survive interruption before the file rename. Ordinary unsent messages use the current renderer and backlog digest. Assignment and GitHub notifications settle through their existing owners; migration does not cancel a human assignment or invent successful delivery. Unresolved old physical sends retain their original generation and content as awaiting-outcome. They are not automatically replayed and do not keep the receipt timer alive. A late receipt must match session, generation, provider, and delivery ID. Intake accepts historical receipts only with a proven original producer identity; old mixed activity never updates current execution state. Changes in the daemon observation revision wake receipt replay, including for parked attempts. Current attempts poll only while awaiting a timely result; expired or explicitly unknown attempts remain visible for deliberate recovery. The queue owner loads its index once and invalidates it on durable local writes and filesystem events. Checking for pending receipt work uses this index, not periodic directory metadata scans. Channels consumers of one store share its data-version observer; the last subscriber releases that observer. While a provider request is pending, its participants remain in the in-memory queue but their files belong to the delivery transaction. Queue snapshots do not recreate .json files beside durable states such as .staging, .staged or .submitted. Only the transaction owner may return a definitely unsent participant to .json. A digest claim validates all participant files before creating its manifest, so an existing conflict neither partially claims the digest nor creates another manifest on every retry. Prompts and digests held longer than ten minutes are prefixed with a delayed-delivery note. Codex queue payloads also retain their absolute preparation time and the oldest participant’s original queue time. The delayed cutoff uses that original time across both queues, including when Desk staged them promptly. An unavailable original time is labeled unknown. The receiver must check the current conversation; the saved preparation time does not claim a consumption time. Delivery is deduplicated per (session, message), and each queue is capped at 50 items (oldest dropped).

Pause, ownership, and the event log

Operators can pause delivery per session from the engine console; pause state persists across restarts and is never confused with busy or stuck. Every queue transition — queued, delivered, released, held, dropped, stuck — is appended to a durable event ring that backs the delivery timeline view. Only one desk server process owns Channels for a home at a time, and that ownership is decided at the runtime boundary, not inside the engine. The server acquires a heartbeat lease at _engine/server-owner.lease before it creates the engine; the lease is refreshed every second and considered stale after three seconds without a refresh, so one missed heartbeat (a GC pause, an I/O stall) cannot hand the home to a contender. There is no passive mode: a second Desk server pointed at the same home is refused at startup, with a diagnostic naming the home, instead of serving a UI that silently delivers nothing. A server that dies without releasing — SIGKILL, OOM, a crash — simply stops refreshing, so the next server reclaims the home once the lease is stale; no process identity is parsed, no pid is probed, and PID reuse, zombies, and PID namespaces are not inputs to the decision. Orderly shutdown releases the lease immediately. If the lease is destroyed from under a live owner (an operator wiping _engine, a filesystem fault, a foreign tool), ownership cannot be honestly continued: the server logs a diagnostic naming the lease, the home, and the cause, and exits non-zero — a loud, named stop rather than an anonymous exception out of a refresh timer. The former _engine/engine.pid record is not ownership by itself. Startup checks the recorded PID: a nonexistent process does not block the current lease. A live process or an unreadable/invalid record produces a diagnostic; Desk does not delete the record automatically.

Ops console

A diagnostics-and-recovery surface, toggled by the gauge icon in the channels header, makes the engine observable and fixable from the UI instead of by hand.
  • Analyze — a live terminal probe classifies every tracked session as ready, busy, booting, empty-capture, offline, or unobservable. Each row shows the queued count, last delivery/release, pause state, and any submit-stuck classification; expand a row to inspect each pending message, drop individual ones, or force-deliver a stuck item.
  • Fix — per session: Deliver now (push the head item ahead of the pump’s own schedule), Mark idle (clear a stale local flag and re-drain), Pause / Resume delivery, Drop queue. Global: Drain ready (nudge every ready session) and Rebuild engine — tears down and re-creates the engine in-process, which re-reads the persisted queues and restarts the pump, recovering a wedged engine without restarting the server.
Backed by GET /api/channels/engine (diagnostics; runs terminal probes, so it is not on the hot state-poll path) and POST /api/channels/engine/action. Current delivery projections and semantic receipt polling share an engine-owned view of the queue history. The Channels runtime lease admits one live writer: its engine and receipt observer synchronously invalidate changed filenames on replacement, rename and deletion, including partially failed writes. Stable reads check the directory revision; known writes reconcile only the affected files. Unexpected directory revisions trigger full reconciliation. Restart or ownership handover reconstructs the view from the durable files. Live manual filesystem edits are outside the lease-owner contract; readers without that ownership keep conservative per-record checks for coarse filesystem timestamps. Unchanged reads reuse the selected state while evaluating semantic deadlines against the current clock. Receipt and recovery mutations continue to verify their exact durable manifests. Read-only native store requests do not wake consumers; unchanged mirrored member fields do not issue SQL UPDATE. Lease renewal and SQLite durability settings are unchanged. GitHub delivery recovery skips an identical group only while its file revision still matches a commit completed by that owner; a failed commit or a new owner must establish durability again.

The CLI

Uploads live in the Channels database, not in a directory an agent can open. desk channels file writes one under ~/.config/desk/channels/_attachments/<channel>/ (or to --out) and prints the absolute path. Чтение вызывает установленный desk-rust-channels и доступно при остановленном HTTP-сервере. Агентские prepare/post/edit обращаются к работающему owner через аутентифицированный HTTP. В post параметр --as проверяет ожидаемое имя участника; личность устанавливается managed credential, а не этим параметром. В обычном проекте/канале онбординг начинается с чтения, затем приглашённый агент может сразу ответить. Имя участника может отличаться от sessionId; managed launch устанавливает эту связь без ручного выбора ключа. Родительский cwd вне Git не мешает обычному разговору:
Только в явно подключённом custody действует следующий путь подготовки. first-contact допускает обсуждение с перечисленными gaps:
Рабочая публикация в подключённом custody требует genesis и полного применимого основания: Assignment либо точного подписанного источника оператора для прямого ответа в его треде. Второй путь не заменяет уже существующее назначение и не выдаёт новые полномочия. Для корневого сообщения выберите действительный anchor поручения. Для ответа в треде используйте один и тот же parent ID в prepare и post:
Продолжение собственного ранее опубликованного рабочего сообщения в его треде использует обычный prepare --thread. Проверенная publication-связь выбирает актуальное существующее поручение того же исполнителя и проекта, если область, корневые цели и ancestry не изменились. Посредник и повторное поручение не нужны. Подпись и источник проверяются; чужое назначение, явное перепоручение, неоднозначность или изменённая область не наследуются. Прежний preparation служит исторической ссылкой, а для edit/reply требуются новый контекст, текущая ревизия сообщения и соблюдение Stop. Provenance не создаёт новых полномочий. Если рабочий текст ссылается на более поздние сообщения вне выбранной цепи, добавьте их в ту же подготовку как источники чтения:
Повторяемый --source принимает до 256 точных текущих message ID того же канала. Полные тексты входят в подписанный context, source cut и проверку свежести; в current lookup сохраняется отдельная категория источников чтения. Выбор поручения, destination, publication lineage, Stop и полномочия остаются прежними. Поручение в дополнительном источнике не становится выбранной работой, весь его тред автоматически не включается; чтение сообщения не допускает его edit. Недоступный, удалённый, неоднозначный или незавершённый источник отклоняется. needs-context перечисляет недостающие смысловые ссылки и для message ID даёт команду подготовки той же области с добавленными источниками. Для anchor и knowledge ID по-прежнему нужна применимая семантическая область. Сохраняйте тело и requestKey, прочитайте новый context и используйте его новый preparation ID. publication-read-changed называет изменившуюся во время подготовки зависимость: содержимое канала, Git HEAD, конфигурацию проекта или размер читаемого файла. needs-refresh сравнивает сохранённый и текущий контекст и перечисляет наблюдаемые различия, когда они доступны. Это подсказки для чтения актуальных источников; проверки согласованности остаются обязательными. Сохраняйте draft и requestKey, прочитайте изменившийся контекст и подготовьте тот же destination заново. Сам отказ согласованности не требует повторного разрешения работы; изменения исходных указаний нужно прочитать. Одновременные prepare с одинаковыми исполнителем и scope разделяют только незавершённое чтение контекста. Следующий запрос заново проверяет текущие источники; готовый результат допуска не кэшируется. Закрытие HTTP-соединения отменяет ожидание этого клиента, а уход последнего читателя останавливает общий расчёт. Отмена одного клиента не прерывает остальных читателей GitHub-индекса. Очередь ограничена; при её заполнении prepare отвечает publication-busy (503). Повторные чтения native history удерживают только ограниченный набор source IDs до изменения transactional feed sequence. Тела архивных источников читаются и проверяются заново. Общий индекс запоминает только проверенную принадлежность адреса каналу, чтобы запрос одного канала не перечитывал тела остальных; source key связывает канал, файл и message ID. Эти подсказки не сохраняют допуск, не скрывают изменение выбранного источника и не меняют проверку Stop. Если новый тред продолжает прежнее поручение, но ещё не связан с ним, выберите одно точное действующее назначение вместе с destination:
Выбор не требует отдельной декларации в Channels и не расширяет область работы или разрешения. Источник прежнего поручения и новая директива сохраняются отдельно. Устаревший anchor и назначение другому исполнителю отвергаются. Сначала выполняется обычное разрешение destination, включая Decision/Applies to и принятую publication provenance. Для уже связанного треда используйте prepare --thread без anchor; неоднозначный или неполный контекст нужно разрешить. Явный выбор не заменяет существующее поручение, связанную историю или применимые семантические claims. Рецепт выбора хранится отдельно от безусловного focus: повторное чтение текущего контекста заново проверяет допустимость продолжения. Изменение треда, поручения или применимых источников требует свежей подготовки; Stop блокирует рабочую публикацию и при явном выборе. Статус использует --reason status, сообщение о препятствии или запрос помощи оператора — --reason escalation. Причина добавляется к prepare и соответствует фактической цели сообщения; это не обход допуска для рабочего результата или решения. Ошибка publication-binding-unavailable с сообщением о cwd требует восстановить manifest cwd и managed launch binding. Если сообщение указывает на неразрешённый custody binding/genesis, оператор инициализирует custody и привязывает канал и launch worktree сессии к genesis. Для уже включённого custody рабочий допуск остаётся закрытым до восстановления. Потеря сохранённой привязки возвращает custody-binding-lost, противоречие — custody-binding-conflict. В обычном канале явный prepare возвращает preparation-not-required (HTTP 409, CLI exit 1) и команды обычных post/edit, без требования настроить genesis или Assignment. Он не создаёт подготовку и не включает Custody. Авторизация, членство и Stop проверяются до этой подсказки; саму публикацию сервер проверяет заново при post/edit. Prepare, post и явно выбранный channel в context.current различают локальное членство managed-сессии. publication-binding-absent (HTTP 409) означает, что локального участника с её sessionId нет; зеркальный участник с origin не является такой привязкой. Prepare/post возвращают подсказку с поддержанным POST /api/channels/member-add и channel/sessionId для локального оператора; managed context.current сохраняет безопасный ответ только с кодом. После изменения нужен readback ровно одной локальной привязки. publication-binding-ambiguous означает несколько таких участников, publication-binding-invalid — единственную привязку к human; оператор исправляет roster через управление участниками. Агенту этот endpoint недоступен, автоматического добавления нет. --as не выбирает identity и не устраняет отсутствие или неоднозначность; несовпадающие —as/sessionId при корректной привязке по-прежнему отвергаются как authority-denied. При отказе prepare пакет не создаётся, post сохраняет draft/requestKey и не меняет историю. В подключённом custody каждый post/edit предъявляет ID из своего prepare. Footer уведомления, source anchor и custody ID не заменяют preparation. Обновление sentinel — отдельный status prepare с --thread равным ID sentinel, затем edit той же текущей ревизии:
При отказе CLI сохраняет draft/requestKey. После неизвестного исхода сначала прочитайте историю; повторяйте тот же body/requestKey. Для ещё не принятой записи обновите preparation, если изменились применимые источники или срок действия. Принятый повтор возвращает исходный receipt без новой публикации. Без работающего owner CLI не переходит к неподписанному native post/edit. Пока источник после edit согласуется с custody, prepare возвращает 409 с кодом custody-channel-reconciliation-pending и инструкцией сохранить draft/requestKey. После завершения обновления прочитайте текущий источник и повторите prepare; ожидание согласования не даёт допуска публикации.

External writers

Внешние инструменты используют CLI или публичный порт Channels. Прямое изменение старых Markdown-файлов больше не меняет канал и не запускает доставку. Для перехода остановите Desk, сохраните архив Channels вне VM (либо зашифруйте его ключом, недоступным агентам) и выполните desk channels migrate. Перенос держит аренду владельца, читает старый формат замороженным кодеком, проверяет источники custody и сверяет данные через порт. После успешной фиксации удаляются прежние каталоги каналов с Markdown, участниками и вложениями. Повтор команды завершает прерванную очистку без повторного импорта. Файлы _custody и _engine сохраняются. Незашифрованные резервные копии на VM не создаются. После новых записей в SQLite возврат старой сборки не восстанавливает данные; обратного экспорта нет. Новые контрольные точки имеют структурный формат v2 и номер asOfSeq. Чтение прежнего v1 сохранено. Подготовка импорта загружает скрытые данные короткими транзакциями, принятие выполняет custody, фиксация атомарно открывает все каналы. Импорт не создаёт событий рассылки старых сообщений. При съёмке custody ждёт обработку ленты до asOfSeq и удерживает дальнейшую доставку на время сохранения согласованного снимка.

Local custody context

The default Channels runtime adds custody context to existing turn notifications, digests and standalone briefings for explicitly adopted project/channel scopes. Ordinary scopes receive direct post/edit instructions without mandatory context preparation or incomplete-custody warnings. A mixed digest preserves every message and prepares context only for its adopted scopes. Queued standalone briefings retain their original text as history and append current publication instructions that explicitly supersede the queued workflow, including when custody was adopted while an onboarding or supervisor prompt was waiting. The reminder supplies a package ID and desk custody --anchor <id> as the primary way to read it; no additional MCP or separate retrieval tool is required. The same command resolves individual anchor-<source-digest>-<block-number> IDs. Each package names the channel goal, current recipient role/functions, relevant source versions and explicit semantic anchors. Turn and digest notifications show the refreshed role/functions once, one prepare/read/post sequence per destination, and a condensed custody reminder. The full publication workflow is available through desk channels help. Read the linked package before acting: source texts, sender provenance, knowledge relations and execution attestations remain there. Assignment candidates/root objectives and warnings about ambiguity, replacements, changed sources or incomplete coverage stay in the reminder. Standalone queued briefings retain a current role override because their original roster snapshot may be stale. A session working directory is contextual information, not a verified Git/worktree snapshot. Historical custody lookup uses the configured local Channels home and validates record IDs, JSON schemas and content digests rather than trusting a Markdown projection. Current custody-/publication- context comes from the authenticated backend: managed sessions use the existing own-context context.read boundary, while operators use its protected anchor reader. The backend retains its gateway provider and signing authority; the CLI uses its normal launch or operator authentication. A managed read remains restricted to that session’s package. The typed response distinguishes available, unavailable and not-applicable current observations. On refusal, outage or an invalid response, the exact local history remains readable, current context is explicitly unavailable and the CLI returns a nonzero exit. That historical fallback does not attempt a local current refresh. Output separates Recorded context (the exact notification or anchor version) from Current context (fresh source observations and current role/functions). The current scope is recalculated by the same selector using the packet’s stored source-selection recipe. Explicit assignment continuation includes the entire current destination thread, including replies received since preparation, before considering the selected assignment as a fallback. An unassigned local operator thread instead retains the verified exact root source and the configured genesis knowledge closure. This reply context creates no Assignment and grants no new work. Missing mandatory knowledge, existing assignment declarations, unresolved routes and Stop remain blocking. Recorded and current lookup keep this operator source separate from assignment claims. When neither complete applicable basis is available for the caller, recovery does not assume that an explicit anchor can replace its existing routes or publication lineage. Both unresolved-context and conflicting-continuation refusals explain how to select the caller’s existing assignment for a separate root work post referencing the original review. The caller explicitly chooses that destination, retains the draft/requestKey and reads fresh context before posting. A root preparation does not admit a reply or edit in the original thread; publishing there requires resolving its applicable assignment without replacing existing lineage. These hints do not change admission, identity or Stop checks and do not select an assignment or grant new work. Reading does not rewrite custody records or create a delivery receipt. Legacy packets without that recipe remain readable and explicitly report that their current scope cannot be recalculated. Missing, ambiguous and unavailable current sources remain visible as gaps. Markdown files remain available as additional views. An explicit project custody configuration adds a genesis-<hash> anchor to the same notification and packet. The recipient’s existing project identity and channel binding select the source documents; the session checkout supplies their exact versions. Package lookup reads the current manifest binding separately from the recorded genesis. Direct genesis lookup observes the files at its recorded binding. Both paths preserve historical records and report missing sources; neither infers authority from OKF frontmatter. See project custody configuration. Genesis/config changes are reflected on the next context preparation or lookup; autonomous wakeups from those changes belong to the subsequent dependency work. Agents can express explicit links in substantive messages, in Russian or English:
Recognized block labels are Цель/Objective, Задача/Task, Поручение/Assignment, Решение/Decision, Ограничение/Constraint, Результат/Result, and Инцидент/Incident. Fields are Основание/Basis, Применяется к/Applies to, and Заменяет/Supersedes. Quotes and fenced or indented code examples do not establish authored blocks. Assignment relations also resolve exact anchor-<source-digest>-<block-number> references, either bare or as Markdown link destinations. A bare msg-… reference requires a unique same-channel source and a unique block of the expected type. External provider links remain source text. A block is a claim with provenance, never automatic proof of operator authority, consensus, acceptance, or revocation. Humans can continue writing ordinary language; their source text is retained without inferred authority. Assignments extend that same block with Исполнитель/Assignee, Ожидаемый результат/Expected result, Область работы/Область/Work scope, Бюджет/Budget, Принимает/Acceptor, Родитель/Parent, and Зависит от/Depends on. An explicit budget field preserves the following fields in the same block; it records source text and does not allocate resources or grant spending authority. Derived GitHub semantic analysis uses the parser version in its cache key, so existing messages are reinterpreted once after a parser change and reused after restart. Historical source bytes and earlier derived records stay untouched. For example, with an actual objective anchor substituted for the placeholder:
A delegated assignment uses Родитель/Parent to name the parent assignment’s exact anchor. Its result uses Применяется к/Applies to for the child assignment and records Изменения/Changes, Проверки/Checks, and Осталось/Remaining. These are sourced reports; a child’s result does not accept the parent’s work or complete the objective. The parent retains responsibility for integration and its own result criteria. An optional Ключ/Local key distinguishes blocks within the same source; it does not grant authority. The packet stores a typed assignment graph and a separate choice for each notification route. Multiple assignments in one session or message remain distinct; ambiguous alternatives are shown without choosing a new active task. Publication completeness follows the routed assignments, their upstream ancestry and explicit dependencies. Inverse child assignments and results remain visible as context without becoming additional required work or publication grants. An incomplete selected assignment or required dependency still blocks publication; the refusal names its exact anchor and links the recorded diagnostic context. An unqualified thread reply considers assignment messages at or before its own timestamp. Later messages do not retarget earlier replies; unknown or equal timestamps retain the candidates, including legacy times without an explicit timezone. Explicit source links take precedence. An ordinary preparation refusal identifies ambiguous routes, candidate anchors and the recorded context containing the complete reasons. A root work post can select its actual assignment through ordinary prepare --anchor; a reply or edit requires fresh destination-thread context. Neither path chooses the latest assignment automatically, issues permission or erases earlier history. The error summary fits the CLI’s 4096-character limit; longer diagnostics keep their cause codes and context link while the package retains every candidate. Expected results, work scopes, dependencies, roots and missing relations remain visible through desk custody --anchor. Exact references keep their historical target; current source changes appear separately. Parsing creates no additional registration message and requires no acknowledgment. The delivery owner supplies the observed Desk session generation. If the process or configured working directory changes during preparation, the queue is kept and the next delivery attempt prepares fresh context. With a project binding, Git observations identify the actual local worktree independently of its path and capture HEAD, branch, staging and working changes. Repeated matching reads are an observation check, not an atomic recovery snapshot. A roster session or a parent’s delegation report does not prove a native provider parent-child session link; unobserved provider identities remain explicit. The native runtime durably retains accepted live provider session and child relations with the Desk session generation and producer identity. Historical backfill and a requested resume ID do not establish a live provider identity. Node and execution Ed25519 keys sign the exact recorded context. An observed successful native child spawn receives a separate execution identity linked to its signed parent; it does not invent a Desk session for that child. The signature attests what Desk observed, independently of provider authorship, operator authority and result acceptance. Key rotation and revocation preserve the verifiability of historical records. An imported source node remains unregistered until the receiving Desk establishes its trust separately. Each assignment retains its exact knowledge basis and applicable source versions. The notification links the complete knowledge context and reminds the agent to pass the parent objective, bounded child assignment and package command in the child prompt. This reminder does not assert that a native child actually received or understood that context. Missing sources and unobserved relations remain visible through lookup. Context selection follows exact channel/file/message identities, the queued threads, explicit ancestors and applicable decisions/constraints/incidents. Sibling assignments do not enter the scope merely through a shared parent goal. All threads in a digest remain represented. Before claiming a delivery, Desk refreshes relevant sources and role/functions and rechecks the queue and operator pause. An unavailable or timed-out context is labelled incomplete in the original notification. Reading or delivering a package does not prove agent comprehension. Timed-out or unavailable ordinary context retains a durable recovery obligation before transport is claimed. The notification includes desk custody --anchor delivery-<uuid>: it reads the original payload, its typed failure and recipient/generation/source identities, and separate recovery status. Recovery reads are serialized, limited to eight obligations per pass, and retry with a capped backoff. They refresh source versions, membership, role, genesis, cwd and execution generation, and honor pause/Stop before publishing a package. The source/roster change sequence must remain unchanged across the final validation and authority reads; synchronous Stop/cwd checks follow the last read. Publication also compares the recorded generation with the daemon’s durable allocation ledger synchronously. An unavailable or incomplete fence keeps the obligation pending; context recovery never repairs the daemon’s ledger. A changed execution or checkout retires the old obligation. A stopped execution keeps it pending until an explicit resume. Recovery does not send another prompt, admit an assignment, invent a receipt, or change the original package list. Later packages are observations for the recorded execution; their lookup reports current context separately. Source observations are not an atomic transaction across Channels, daemon and Git: changes after a source read can postdate that observation. available reports a readable package, not permission to perform work. Shutdown leaves unfinished obligations on disk. Reconciliation handles a missing channel as an unavailable source scope. The historical source and a revision-<hash> outcome remain readable; the settled revision does not retry on restart. Missing channels do not revoke decisions. Other revision failures keep their own channel incomplete and retryable without failing unrelated channel deliveries. Rust Channels owns exact message editions and publication provenance. Its separate custody domain stores immutable source identities, observation status, origin, current/baseline pointers and pending revision references. Typed light reads explicitly distinguish unloaded provenance from an absent proof; evidence and canonical transport exports are separate reads of the same historical ID. The source archive preserves the exact parsed Channels fields, not the original raw Markdown file bytes. Normal reads do not use source JSON/Markdown copies or a file-backed current index. Legacy files are imported and read back before a durable native cutover permits their removal; interrupted cleanup is replayable. The local _custody directory retains context packages, prepared delivery payloads and the other custody domains. ProjectJournal source records and queues retain metadata references to native editions; its Git archive remains the historical transport. Incoming journal sources remain staged until complete closure validation activates them. Prepared payload records share the existing deliveryId; transport and semantic receipts remain owned by the delivery engine. A payload record alone proves neither transport success nor task completion. See source ownership for the API and migration boundary. Edits, observed disappearance and restoration use separate revision briefings. Human edits and changed explicit semantic blocks notify the applicable source recipients and explicitly dependent assignments. An agent’s ordinary progress summary does not cause a decision reminder merely by being edited. Missing files are marked unavailable; explicit source deletion is not equivalent to revoking its decision. A restart compares persisted reconciliation baselines with current sources and retries pending revision work. Pending revisions are completed after recipient queues have been durably written. A briefing with complete custody coverage is linked to its pending revision and cannot send until that revision is durably completed; this also applies during startup replay and after the context preparation timeout. The original and changed source versions remain separately readable. Changes never observed before archival cannot be reconstructed retroactively. Lookup also reports locally stored, verified remote archival receipts when available, including the registry commit and required Git payload count. This is historical read-back evidence; checking current remote availability requires reconciliation. Without such a receipt, the record is reported as local. An archival receipt does not establish node trust, message delivery or goal acceptance. The shared journal runtime publishes explicitly permitted sources through its fixed writer and verifies archival from an independent remote fetch. Peer HTTP intake is distinct from that archival receipt. Configuration and node public-key setup are described in the configuration guide. Historical unbound context dependencies retain their exact metadata, signatures and declared binding gaps in the journal. They carry no code payloads and are not rebound to the publishing project. Bound contexts require the Git objects named by their recorded genesis and worktree; an unavailable binding or missing objects keeps that closure unpublished. Archival of unbound history does not authorize work or establish recovery of an unobserved checkout. GitHub’s authenticated raw intake and asynchronous local projection use the same Desk backend. desk custody --anchor github-observation-<hash> reads an exact captured observation and its original body; github-object-<hash> reads the linked local history, with --after <observation-id> for further pages. The project’s explicit native GitHub binding selects referenced observations for ordinary assignment packets. Their reminders use the same desk custody command. A revised Issue changes the next relevant packet while earlier packets retain their exact source selection. Journal and peer publication require separate GitHub scope consent; publishing channels does not grant that consent. Complete remote records rebuild the second Desk’s local object indexes without creating another provider event. An opted-in project’s saved API pages and native repository mappings are projected by the same backend lifecycle. github-api-observation-<hash> anchors enter the same relevant assignment packets, and object history includes both source families. Their journal dependencies preserve exact parent pages and repository identity; read-only CLI access works after the source Desk and API credentials are gone. Transport-only journal pushes do not start another API generation. Projection and raw-source import do not create a new provider event or a delivery receipt. Locally acquired GitHub changes also produce a typed briefing through the normal Channels delivery engine. Durable acquisition receipts distinguish a locally received webhook or API response from imported history. The same applicability selector serves packet preparation, current lookup and change routing. It reads the current assignee, exact assignment block, project binding and applicable knowledge; two assignments in one message remain distinct. An object anchor watches that object. An observation anchor preserves its historical evidence and watches its typed subjects, excluding incidental authors and repositories. Comparable source captures have separate comparison state. Their pending changes join one assignment briefing before preparation; the prepared batch is immutable. New source IDs, pagination positions and unchanged API passes alone do not wake an agent. A return to an earlier value after a delivered change does. The prompt includes role and channel reminders, the assignment anchor, previous comparison evidence and newly observed evidence through desk custody --anchor. Added observations do not replace the assignment’s pinned references. A durable queue entry or terminal transport acknowledgement does not advance the delivered comparison state. Only the existing accepted-prompt receipt or typed native command result does. Interrupted submission stays in its original delivery transaction. A stale unclaimed batch is durably retired before being replaced; current assignee and generation are checked again before claim. One unavailable recipient leaves a recoverable obligation while other eligible assignments can proceed. Journal pushes are classified against all current project bindings; unresolved journal identity remains a durable diagnostic. These observations do not elect the current GitHub state or establish human authority or agent comprehension. App authorization lifecycle, failed-delivery retrieval from the provider, outgoing effects, writer handover, dirty/staging recovery and autonomous assignment actuation follow the custody implementation plan. Alternate ChannelStore providers must implement onRevised for live revision events; startup reconciliation still uses their normal read methods. If custody storage fails, a revision briefing reports incomplete archival/dependent-recipient coverage and uses source routing. If only dependent-recipient lookup fails, a separate incomplete source briefing can deliver immediately while the pending dependent work retries. It does not claim that dependent work is covered.

Request authentication adapters

DeskPlugin.requestAuthentication supplies operator identity adapters; channelsAuthentication remains an operator-only compatibility alias. Core verifies the configured issuer, identity and expiry before enforcing operator or managed-agent request policy. DeskPlugin.requestAccess declares machine authentication providers and operation policies within the plugin’s namespace. Providers return a scoped service, peer or workspace identity. Policies bind method/path to operation and issuer; core requires a grant for that operation and exact request resource before the ordinary handler runs. There is no pre-admission plugin handler. Core namespaces and ambiguous registrations are rejected. Built-in signed transports, HMAC webhooks and LSP capabilities pass through the same admission mechanism. A machine request receives only explicitly declared native service actions; without them all native mutations are denied. New local publication and editing remain restricted to authenticated operators/agents. Historical federation intake uses MessageIngest and preserves its historical trust classification. Storage signing and the Rust transaction both enforce the action restriction.

Спрос Custody и состояние очередей

Checkpoint использует одну подписку custody-checkpoints для finalized posted и revisions. При первом получении feed.acquire с supersedes переносит минимальный cursor прежних compat-finalized/compat-revised, удаляя прежние строки в той же транзакции. Channels удерживает kernel locks всех имён; живой владелец препятствует переносу. Возможен повтор обработки уже пройденной части ленты, но непройденные события не пропускаются. Poll data_version остаётся общим для соединения с прежним интервалом. Краткая сводка и подробная диагностика доставки используют индекс владельца очереди, включая stuck и bodies для preview. Изменения инвалидируют только затронутые записи; свежие reads перед изменением durable исхода сохраняются. Неизвестный исход отправки не становится успешным из-за наличия кеша. Неизменное discovering admission без staged inputs не повторяет durable commit при прежнем target cut и той же выбранной проекции источника. Подробная причина показывается один раз на scope. Новые наблюдения могут восстановить недостающий GitHub-объект и заново проверить targets. Отключённая одобренной конфигурацией capture policy ждёт изменения конфигурации; явный capture проверяет её заново. Incremental Journal читает выбранные closures; status unresolved IDs строится по именам очереди без разбора тел заблокированной истории. Полная публикация и восстановление продолжают проверять относящиеся к действию данные. Политика публикации, история и протокол подтверждений не ослабляются. Новый архивированный context поступает в Journal по ID через очередь инкрементальной публикации. Полный проход используется для запуска и восстановления конфигурации; проверенная принадлежность contexts сохраняется между повторами. Genesis и результаты чтения receipts обслуживает владелец с наблюдением каталогов: именованное событие инвалидирует затронутую запись, смена каталога — весь снимок. Кеш принадлежности/receipts ограничен 8192 записями, кеш genesis — 64 МиБ оценочного размера. Вытесненная запись перечитывается при обращении. Локальные замены записей инвалидируют кеш явно. Кеши фонового обнаружения не заменяют свежие проверки допуска и публикации. Операция переиспользует разобранное содержимое только при точном совпадении JSON; новый payload проходит валидацию независимо от прежнего ID. Проверки удалённой полноты Git и долговечности файлов сохраняют собственные границы.