Skip to content

ADR-001 — Модель исполнения: Instance + Tracks

Поле Значение
Статус Принято
Версия v.6
Дата 2026-06-27
Владелец Руслан Габитов
Замещает v.2 (трёхслойная модель Instance / track / token). v.3 сворачивает её до двух слоёвtoken становится логической проекцией track'а, а не хранимой сущностью — и откладывает персистентность / регидрацию в отдельный ADR.
Уточняет SAD-001 v.1 §10 Execution Model

EN-оригинал — канонический: ADR-001-execution-model.md. Этот файл — его перевод (twin).

1. Контекст

goBpm исполняет процессы BPMN 2.0 по модели token-flow из спецификации. In-memory рантайм, который определяет этот ADR, ОБЯЗАН обрабатывать:

  • Множество параллельных Process Instance (один движок, N экземпляров разных определений процессов исполняются параллельно).
  • Множество параллельных потоков исполнения внутри одного instance (split на Parallel Gateway, Multi-Instance Activity, активации Event-Sub-Process).
  • Кардинально разные временные масштабы в одном instance — ServiceTask за миллисекунды; UserTask, ждущий днями; многодневный Timer. Длинные ожидания НЕ ДОЛЖНЫ удерживать горутину.
  • Предсказуемую мутацию состояния без гонок данных — переходы соответствуют нормативным конечным автоматам BPMN (activity-lifecycle.md).
  • Корректное завершение (Terminate End Event отменяет всю выполняющуюся работу в instance, end-events.md).

Область этого ADR. Он определяет ядро модели исполнения рантайма — как instance и его потоки исполнения живут, форкаются и отменяются в памяти. Концерны, надстраивающиеся над этим ядром, принадлежат отдельным ADR и здесь не специфицируются (см. §9): семантика join/merge (ADR-005), доставка событий и отмена по событию (ADR-006), in-memory модель освобождения на длинном ожидании (ADR-007). Персистентность и восстановление — first-class P0 требование (SAD-001 §6), отдельный тяжёлый концерн, отложенный в выделенный Persistence & State ADR; §4.7 фиксирует только инварианты уровня рантайма, которые персистентность обязана сохранить.

Текущая кодовая база уже содержит развитую модель в internal/instance/ (Instance + track + token + stepInfo). Этот ADR формализует рантайм, которому служат эти типы, и сворачивает token из хранимого типа в проекцию — см. §3.1 и §6.

2. Решение

Двухслойное владение. Instance владеет одним или несколькими track'ами. track — операционный поток исполнения (одна горутина), несущий свою текущую позицию во flow и состояние. token — контрольная позиция из спецификации BPMN, выраженная как логическая проекция текущего шага track'а (track.Token(), Instance.GetTokens()), а не хранимый объект. Instance держит только реестр track'ов — реестра токенов нет; «instance завершён» означает, что все track'и закончились. Состояние уровня instance мутируется только внутри единственной горутины event-loop'а Instance; track'и сообщают о прогрессе событиями в канал и никогда не мутируют состояние Instance напрямую. Форк создаёт новый track на каждую дополнительную ветку (1:1 между track'ом и его позицией), родительский track продолжает по первому flow. context.Context — контракт отмены. Персистентность и регидрация специфицируются отдельно; этот ADR — in-memory рантайм, на котором они строятся.

В сводной форме:

Концерн Механизм
Владение состоянием instance Instance регистрирует и координирует track'и; мутирует состояние instance только в своём сериализованном event loop
Исполнение потока track работает в выделенной горутине; ведёт конечный автомат шага по последовательности узлов
Семантическая позиция BPMN tokenпроекция текущего шага track'а (позиция + производное состояние + происхождение); вычисляется по запросу, не хранится
Механика форка родительский track продолжает по первому flow; один новый track на каждый дополнительный flow, каждый в своей горутине
Безопасность мутации состояния состояние instance мутируется только внутри его event-loop-горутины (без блокировок); состояние track'а — только внутри его собственной горутины
Отмена каскад context.Context: Engine → Instance → track
Происхождение (lineage) хранится на track'е (track.prev); родительство при форке — это родительство track'а
Join / события / длинные ожидания вне области здесь — ADR-005 / ADR-006 / ADR-007 (см. §9)
Персистентность / рестарт отложено — выделенный Persistence & State ADR

3. Рассмотренные альтернативы

3.1 Альтернативы слоёв

Вариант Описание Вердикт
Один слой (Instance + token) У каждого токена своя горутина и конечный автомат. Отклонено. Теряется декомпозиция стадий исполнения (prologue / execute / epilogue / await-results), которую несёт модель stepState; перегружает токен и семантикой BPMN, и управлением горутиной.
Три слоя (Instance + track + token-как-объект) Track владеет горутиной + автоматом шага; token — отдельный живой объект, владеющий позицией BPMN + происхождением, 1:1 с track'ом. Отклонено (был выбор v.2). При 1:1 без миграции живой token-объект лишь дублирует track: вынуждает двунаправленные обратные ссылки (token.inst, token.trk), второй реестр (Instance.tokens[] рядом с tracks) и дублирующую цепочку происхождения (token.prevs/nexts, зеркалящую track.prev). Отдельная идентичность токена нужна только как сериализуемое значение во время персистентности — это решает Persistence ADR — а не как живой конкурентный объект.
Два слоя (Instance + track; token как проекция)выбрано Track — операционный поток, несущий позицию + состояние; token — read-model, проецируемый из текущего шага track'а. Выбрано. Убирает клубок перекрёстных ссылок и дублирующие реестры/происхождение; инвариант 1:1 держится по построению (нечего синхронизировать). Token выживает как словарь BPMN и как проекция/сериализуемое значение, а не как хранимая сущность.
Четыре слоя (явный Scope как уровень владения) Scope как слой владения между track'ом и позицией. Отклонено. Scope (internal/scope/) — контекст разрешения данных, а не уровень владения.

3.2 Механика форка

Вариант Механика Вердикт
A. Форк держит N позиций на одном track'е Одна горутина ведёт параллельные ветки последовательно Отклонено. Нет реального параллелизма. Текущий token.split(n) держит split на том же track'е — именно это поведение меняет v.3.
B. Форк создаёт новый track на каждую доп. веткувыбрано Каждая параллельная ветка получает свою горутину; родитель продолжает по первому flow Выбрано. Истинный параллелизм; соответствует «конкурентным потокам исполнения» BPMN; стоимость горутины пренебрежимо мала.
C. По политике (track'и или позиции) Instance выбирает Отклонено. Поверхность политики без выигрыша; ограничения конкурентности можно навесить семафором над вариантом B.

3.3 Мутация состояния instance

Вариант Механика Вердикт
Общее состояние + mutex Track'и держат указатель на Instance и мутируют поля под блокировкой Отклонено. Класс багов lock-дисциплины; зависимость от race-детектора; конкуренция при сильном fan-out. (Это форма текущего кода.)
Сериализация через event-loopвыбрано Instance владеет своим состоянием; track'и шлют события в канал; одна горутина Instance применяет их по порядку Выбрано. Мутация одним владельцем — без гонок по построению, без блокировок на состоянии instance.

3.4 Драйвер горутин

Вариант Вердикт
OS-процесс на instance Отклонено — стоимость старта, IPC, ломает встраиваемость.
Один глобальный реактор на все instance Отклонено — один медленный узел блокирует все instance.
Горутина-на-trackвыбрано Нативная конкурентность Go; соответствует модели конкурентных потоков BPMN.

4. Детали решения

4.1 Роли

Instance

Граница владения. Одна горутина крутит event loop; она — единственный писатель состояния instance.

  • Держит реестр живых track'ов (карта активных потоков исполнения). Реестра токенов нет.
  • Принимает track-события (прогресс / форк / завершение) в канал и применяет их по порядку.
  • Порождает новые track'и в точках форка.
  • Владеет корневым context.Context; отмена каскадируется на все track'и.
  • Решает завершение instance: instance завершён, когда все track'и закончились (в реестре не осталось активных), либо при отмене контекста / остановке.
  • Проецирует токены по запросу: Instance.GetTokens() возвращает токен-вид каждого активного track'а.

track

Один поток исполнения. Одна горутина на track на всё его время жизни.

  • Несёт свою текущую позицию во flow (узел, на котором стоит) и состояние уровня track + уровня шага.
  • Ведёт жизненный цикл шага для текущего узла: prologue → execute → epilogue → await results.
  • Исполняет поведение BPMN узла через его NodeExecutor.
  • Читает входы из Scope, пишет выходы обратно; эмитит track-события Instance на каждом наблюдаемом BPMN-переходе.
  • Несёт происхождение форка (track.prev).
  • Обрабатывает прерывание через ctx.Done(); чисто завершается при завершении, ошибке, merge или отмене.
  • Выставляет свою позицию как проекцию токена: track.Token().

token (проекция — не хранимый тип)

Token — контрольная позиция управления из спецификации BPMN, выраженная как read-model, проецируемый из текущего шага track'апозиция узла + производное состояние + происхождение. Вычисляется по запросу (track.Token(), Instance.GetTokens()), никогда не хранится как живой перекрёстно-связанный объект и не имеет обратной ссылки на свой track или на Instance. «token» остаётся словарём проекта и BPMN (события, аудит и — позже — единица, сериализуемая персистентностью, именуются токеном); ушёл только живой объект.

4.2 Конечные автоматы

Жизненный цикл instance: Created → Active → Completed, с веткой отмены Active → Terminating → Terminated (instance.State). Ветка ошибок (Failing/Failed) и приостановка (Paused) принадлежат своим будущим ADR и намеренно отсутствуют в рантайме (см. §9).

Жизненный цикл track'а (сохранён из текущего кода):

TrackCreated → TrackReady → TrackExecutingStep → TrackProcessStepResults → TrackWaitForEvent
                                                      ↓
                                          [TrackMerged | TrackEnded | TrackCanceled | TrackFailed]

Рантайм производит терминальные состояния track'а TrackEnded / TrackCanceled / TrackFailed. TrackMerged (синхронизирующий join) и причину завершения withdrawn (проигрыш гонки на Event-Based Gateway) производит ADR-005; их значения enum существуют, но в этом ядре не имеют производителя.

Жизненный цикл шага (сохранён):

StepCreated → StepStarted → StepPrologued → StepExecuting → StepEpilogued → StepAwaitsResults → StepEnded → (StepFailed)

Состояние токена как проекция. Состояние токена не хранится; это чистая функция от состояния track + шага:

Token (логически) Проецируется из (tokenStateFor)
Alive TrackReady / TrackExecutingStep / TrackProcessStepResults
WaitForEvent TrackWaitForEvent
Consumed TrackEnded / TrackMerged / TrackCanceled / TrackFailed
~~Withdrawn~~ снято — Event-Based gateway маршрутизирует без создания токенов на плечах, поэтому токен Withdrawn не производится (ADR-005 v.4 §2.12.1)

Это заменяет хранимый enum TokenState из v.2 производным видом. (Значение TokenWithdrawn когда-то было зарезервировано под проигрыш гонки на Event-Based Gateway; ADR-005 v.4 §2.12.1 сняло его — шлюз маршрутизирует, никогда не помещая токен на проигравшее плечо, так что проецировать Withdrawn не из чего.)

4.3 Топология каналов (event loop)

Построенная форма — один входящий поток событий (track → Instance):

type Instance struct {
    ctx    context.Context

    events chan trackEvent    // tracks -> loop()  (evFork / evEnded)
    tracks map[string]*track  // мутируется ТОЛЬКО в loop()
    state  atomic.Uint32      // run-состояние; пишется только loop(), читается lock-free
    // ... без реестра токенов
}

func (i *Instance) loop(ctx context.Context, initial []*track) {
    // порождает начальные track'и, затем дренаж пока все track'и не закончатся:
    for active > 0 {
        select {
        case <-ctx.Done():
            stopAll()              // сигнал каждому track'у; loop продолжает дренаж
        case ev := <-i.events:     // evFork -> построить track на каждый доп. flow
            ...                    // evEnded -> active--
        }
    }
    // все track'и закончились -> Completed, либо Terminated, если завершение вызвала отмена
}

Второе входящее ребро (EventHub → Instance для доставки Message / Timer / Signal) добавляет ADR-006; оно не часть этого ядра. Наблюдаемый BPMN, токен-ориентированный вид (split / merged / waiting / consumed / withdrawn) выводится из этих событий для аудита (и, позже, для чекпойнтов персистентности) — это не второй живой канал.

4.4 Механика форка

Точка форка — любой FlowNode с N>1 исходящими sequence flow, которые становятся активными — не только эксклюзив-шлюз (Activity с несколькими исходящими flow — это неуправляемый split, token-flow.md).

  1. Track A исполняет узел форка; его активные исходящие flow — F1…FN (в порядке объявления).
  2. Track A продолжает по F1 — его позиция переходит к цели F1; A не заканчивается.
  3. На каждый оставшийся Fₖ (k=2…N): Instance конструирует новый track на узле-цели Fₖ, с track.prev = A (происхождение), регистрирует его и запускает его горутину.
  4. После форка N track'ов работают независимо — 1 исходный + N−1 новых — каждый со своей позицией, каждый в своей горутине.

Какие исходящие flow активируются по типу шлюза (parallel / inclusive split / неуправляемый split активности), определяет ADR-005; это ядро форкается по тем flow, которые узел сообщает активными.

4.5 Механика join — вне области

Семантика join/merge (синхронизирующий join, несинхронизирующий merge, OR-join, Event-Based Gateway) не часть этого ядра рантайма. Она определена в ADR-005 Gateways & Joins. Сегодня в рантайме нет учёта join: узел, до которого дошли несколько track'ов, исполняется по разу на каждое прибытие.

4.6 Каскад отмены через context

  • Контекст Engine владеет всеми контекстами Instance; контекст Instance производен от него; контекст track'а — от контекста Instance.
  • Остановка Engine → отмена всех контекстов Instance → каскад.
  • При отмене loop сигналит каждому track'у (stop()), продолжает дренировать их терминальные события и достигает Terminated, когда все вышли; нормальный дренаж достигает Completed.

Узлы BPMN, запускающие этот каскад — Terminate End Event (отменяет весь instance) и прерывающие boundary-события (отменяют один track) — принадлежат ADR-006; это ядро владеет механизмом каскада, а не его BPMN-триггерами.

4.7 Инварианты рантайма для длинных ожиданий и персистентности

In-memory модель освобождения на длинном ожидании (горутина ждущего track'а заканчивается; при приходе триггера порождается свежий track) принадлежит ADR-007 In-Memory Long Waits, а долговечная версия (переживающая рестарт) — Persistence & State ADR. Это ядро фиксирует только инварианты рантайма, которые обе обязаны соблюдать:

  • Состояние продолжения track'а полностью описывается его позицией (узлом), состоянием track/шага, данными Scope и происхождением — нет скрытого состояния на отдельном объекте-токене.
  • Узел с возобновляемым in-flight состоянием (позиция таймера, подписка корреляции, частичное состояние активности) владеет формой этого состояния. Владение состоянием рантайма решено в ADR-009 v.1: каждый instance клонирует шаблон процесса в свой собственный приватный граф узлов, так что состояние рантайма на каждый instance живёт на собственном узле этого instance — разделяемые определения шаблона остаются неизменяемыми. Долговечная персистентность этого состояния — его сериализация и регидрация через рестарт — остаётся концерном Persistence & State ADR.

5. Последствия

Плюсы

  • Верность словарю BPMN без накладных расходов. Token остаётся концептом спецификации; track — операционный примитив; токен — просто спроецированная текущая позиция track'а.
  • 1:1 по построению. Нет второго реестра, нет дублирующего происхождения, нет обратных ссылок token↔track/instance для синхронизации.
  • Состояние instance без гонок. Один владелец-event-loop; без блокировок на состоянии instance; race-детектор (теперь гейтит CI) это подтверждает.
  • Нативно для Go. Горутины + каналы + контексты; без фреймворка.
  • Переиспользует существующую структуру. track, stepInfo, trackState, stepState переносятся; работа — в удалении типа token и реактивных/lock-путей, а не в изобретении машинерии.

Минусы / что нужно соблюдать

  • Число горутин = сумма активных track'ов. Ограничено структурой BPMN; патологические модели можно ограничить семафором над путём форка (оптимизация, не смена модели).
  • Дисциплина каналов обязательна. Каждый track ОБЯЗАН эмитить терминальное событие до выхода своей горутины, иначе утечка. Митигация: defer-очистка; тесты проверяют, что runtime.NumGoroutine() возвращается к базовой линии.
  • Нет справедливости между track'ами. Порядок решает планировщик Go; тесты token-flow НЕ ДОЛЖНЫ зависеть от порядка планирования горутин.
  • Терминальные события всегда должны доставляться. evEnded track'а учитывается даже во время отмены (loop дренирует до терминального состояния); emit отбрасывает событие только после выхода loop'а. (Само различие причин withdrawn/canceled приходит с ADR-005.)

6. Концепция vs текущий код — намеренные расхождения

Только рантайм (расхождения по персистентности уходят в Persistence ADR). Реализация приземляется по SRD этого рефакторинга.

Тема Текущий код Этот ADR (v.3) Требуемое изменение
Token как тип структура token с inst, trk, prevs, nexts, state; Instance.tokens []*token Хранимого токена нет; token — проекция текущего шага track'а Удалить тип token и Instance.tokens. Добавить track.Token() / Instance.GetTokens(), возвращающие вычисленное значение Token (read-model).
Кардинальность track:token token.split(n) делает N токенов на том же track'е (newToken(t.inst, t.trk)), затем checkFlows переназначает Track и есть одна позиция; форк делает новые track'и Убрать split; форк конструирует новые track'и прямо на цели каждого доп. flow.
Владение Instance ↔ token Instance.addToken / tokenConsumed; обратная ссылка token.inst; token.updateState вызывает наверх в Instance Instance держит только track'и; обратных ссылок токена нет Убрать addToken / tokenConsumed / token.inst. «Instance завершён» = все track'и закончились (реестр пуст/терминален), решается в event loop.
Происхождение дублировано: track.prev и token.prevs/nexts Единая цепочка на track'е Оставить track.prev; убрать происхождение токена.
Мутация состояния реактивные методы на Instance под sync.RWMutex Одна горутина event-loop; track'и шлют события; без блокировок на состоянии instance Добавить Instance.loop() + топологию каналов; перевести прямые мутации в события.
Состояние токена хранимый enum TokenState на токене Производная проекция из состояния track/шага (tokenStateFor) Сделано — enum убран из токена; вычисляется в проекции. Причина завершения withdrawn перенесена в ADR-005.
Жизненный цикл instance 9 ad-hoc состояний (Created/Ready/StartingTracks/Runned/Stopping/Paused/FinishingTracks/Finished/Canceled), часть не используется Created → Active → Completed; ветка отмены Terminating → Terminated Сделано — enum приведён к словарю §4.2; ошибки (Failing/Failed) и приостановка (Paused) перенесены в свои будущие ADR (§9).

Известная проблема (перенесена). Исполнение узла вызывает NodeDataLoader.RegisterData, который сейчас мутирует разделяемый узел (например, EndEvent.dataPath) — нарушение неизменяемости из §4.7, дающее гонку, когда два track'а проходят один узел (вскрыто тестом несинхронизирующего merge). Исправление — контракт состояния/загрузки данных на узел, принадлежащий Persistence & State ADR; поэтому несинхронизирующий merge над разделяемым узлом отслеживается в ADR-005, а не заявляется гейтом §7 этого ядра.

7. Верификация

Как мы знаем, что реализация соответствует концепции — приёмочный гейт ядра рантайма. Все строки ниже проверены и зелёные (тесты в internal/instance/, прогон под -race в CI); это доказательство, стоящее за статусом «Принято».

Что Как Чем проверено
Свобода от гонок Все тесты под -race (гейт CI). Состояние instance мутируется только в loop(); любая гонка — блокирующий провал CI. весь пакет под -race
Без утечки горутин Хелпер проверяет, что runtime.NumGoroutine() возвращается к базовой линии после завершения. leakcheck_test.go
Форк 1:1 Split на 2: независимые track'и, каждый со своей позицией; родитель продолжил по F1 (не закончился); ребёнок ответвился после форка. TestM4ForkCompletes
Нет реестра токенов Токены доступны только через GetTokens() (проекция из track'ов), поля tokens нет; track.Token() отражает текущий шаг. TestTokenStateProjection, TestM3*
Завершение instance Instance достигает Completed ровно когда все track'и закончились — не сканированием живых токенов. TestM2LinearCompletes, TestM4ForkCompletes
Каскад завершения Отмена контекста останавливает каждый track и дренирует его горутину в пределах границы; instance достигает терминального состояния. TestTerminationCascade

Перенесённые гейты принадлежат своим ADR: синхронизирующий join / несинхронизирующий merge → ADR-005; in-memory длинное ожидание → ADR-007; триггеры Terminate End Event / boundary → ADR-006. Тесты восстановления после рестарта и долговечных чекпойнтов принадлежат Persistence & State ADR.

8. Ссылки

9. Вне области — принадлежит будущим ADR

Этот ADR — ядро рантайма. Следующее надстраивается над ним и специфицируется в выделенных ADR (боковые ссылки, согласованные по иерархии); у каждого свой приёмочный гейт, и приземляется он со своим SRD + кодом:

Концерн Владелец Статус
Семантика join/merge — синхронизирующий join, несинхронизирующий merge, OR-join, Event-Based Gateway + причина Withdrawn; активация flow форка по типу шлюза ADR-005 v.4 Gateways & Joins Accepted
Доставка событий (EventHub → Instance), Terminate End Event, прерывающие boundary-события, узлы ожидания ADR-006 v.1 Events & Subscriptions Accepted
In-memory модель освобождения на длинном ожидании (подписка → горутина заканчивается → повторное порождение) ADR-007 In-Memory Long Waits Draft
Долговечная персистентность и восстановление после рестарта; контракт состояния на узел (исправляет мутацию разделяемого узла RegisterData, §6) Persistence & State ADR (будет написан)
Состояния ошибок instance (Failing/Failed) и приостановка (Paused) будущие Error-Handling / Persistence ADR

Политика «ADR на каждый адаптер». Каждый существенный адаптер расширения — персистентность (Repository), наблюдаемость (OpenTelemetry), обмен сообщениями, авторизация, диспетчеризация воркеров — специфицируется собственным ADR, а не целиком внутри ADR-002 или скелетного SRD (тривиальным умолчаниям вроде no-op или slog ADR не нужен). Скелет расширений (SRD-004) приземляет только минимальные, in-memory контракты-умолчания, нужные, чтобы исполнять сегодняшний BPMN на этой двухслойной модели; интерфейс Repository, как он определён в ADR-002 v.1, достаточен для этой цели миграции (только in-memory). Production-grade контракт каждого адаптера — для Repository: долговечная сериализация, версионирование/CAS, транзакции, history/inbox/subscriptions, пагинация — принадлежит его выделенному ADR (Persistence & State ADR выше).

История документа

Версия Дата Автор Изменение
v.6 2026-06-27 Руслан Габитов Фиксирует дозревание принципа единственного писателя (§2; §4.1 — track'и никогда не мутируют состояние Instance напрямую) и его §7-гейта свободы от гонок: владение теперь охватывает не только мутацию состояния instance, но и кросс-горутинные чтения — loop является единственным читателем разделяемого вида позиций токенов / join'ов, поэтому живой track больше не выставляет мутабельное состояние для чтения другой горутиной. Гонка чтения с чужой горутины, которую прежде латали по местам, тем самым устранена по построению; гейт теперь прогоняется в масштабе под -race (гейтится в CI, включая продлённые конкурентные стресс-прогоны). Концепция этого ядра не изменилась — реализующая это подсистема доставки событий принадлежит своему выделенному ADR (строка доставки событий в §9). На bump'е консолидированы устаревшие исходящие пины (§8 ADR-005 v.2→v.4; §9 статус ADR-005/006 →Accepted, проставлены пины). RU-twin синхронизирован до v.6 в этом change-set'е.
v.5 2026-06-11 Руслан Габитов §4.7 согласован с ADR-009 v.1: владение состоянием рантайма на каждый узел теперь решено (каждый instance клонирует шаблон процесса в свой собственный приватный граф узлов; состояние рантайма на каждый instance живёт на собственном узле этого instance; разделяемые определения шаблона остаются неизменяемыми), тогда как v.4 откладывала его целиком в Persistence ADR. Долговечная персистентность/сериализация/регидрация остаётся за будущим Persistence & State ADR. В остальном модель исполнения не изменилась. Синхронизация RU-twin отложена (пакетно).
v.4 2026-06-08 Руслан Габитов Зафиксирована политика «ADR на каждый адаптер» (§9): каждый существенный адаптер расширения получает свой ADR; скелет SRD-004 поставляет только минимальные in-memory контракты-умолчания, чтобы исполнять текущий BPMN на двухслойной модели, а production-grade контракты (в частности долговечный Repository — сериализация, версионирование, транзакции, history/inbox/subscriptions, пагинация) отложены в ADR на каждый адаптер. Минимального Repository из ADR-002 v.1 достаточно для цели in-memory миграции. Модель исполнения не изменилась. Синхронизация RU-twin отложена (пакетно, до устаканивания текущего раунда правок).
v.3 2026-06-07 Руслан Габитов Принято. Свёрнута трёхслойная модель до двух слоёв (Instance + track); token становится логической проекцией текущего шага track'а, не хранимым типом (убраны token.inst/trk, Instance.tokens[], дублирующее происхождение). Принята одна горутина event-loop для мутации состояния instance (без блокировок). Этот ADR ограничен ядром рантайма — join/merge (ADR-005), доставка событий и триггеры (ADR-006) и in-memory модель освобождения на длинном ожидании (ADR-007) перенесены в выделенные ADR (§9), чтобы документ был равен коду. Жизненный цикл instance приведён к Created → Active → Completed (+ Terminating → Terminated). Гейт §7 проверен и зелёный (свобода от гонок, без утечек, форк, проекция, завершение, каскад завершения) — включая два бага, вскрытых гейтом (гонка track.stopIt; emit, отбрасывавший evEnded при отмене). Персистентность/регидрация отложена в Persistence & State ADR. Pre-acceptance Draft-итерация свёрнута без построчных записей.
v.2 2026-05-29 Руслан Габитов Трёхслойная модель Instance/track/token (заменена v.3).