Поток сообщений
TUI — «глупый терминал»: отправил в шину, ждёт от агента. Агент решает: начать обработку сразу или поставить в очередь.
События
SendMessageEvent {sessionID, messageID, message}
Издатель: TUI (через onSend)
Подписчик: SendMessage Handler в app_runner.go
TUI публикует это для каждого пользовательского сообщения (текст, команда, скилл, ответ ask_user). Не рисует ничего — ждёт ответа от агента.
MessageQueuedEvent {sessionID, messageID, display}
Издатель: SendMessage Handler (когда TryStart failed — сессия занята)
Подписчик: TUI → handleMessageQueued → рисует в pending-очереди
MessageReceivedEvent {sessionID, messageID, message}
Издатель: Agent (Run для прямого сообщения, drainPending для pending)
Подписчик: TUI → handleUserMessageReceived → рисует в чате + убирает из
pending по messageID
Сценарий 1: сессия свободна
sequenceDiagram
participant U as Пользователь
participant TUI
participant H as Handler
participant M as Manager
participant A as Agent.Run
participant DB as SQLite
U->>TUI: печатает "привет"
TUI->>H: SendMessage{msg-1, "привет"}
Note over TUI: НЕ рисует!
H->>M: TryStart(sessionID)
M-->>H: true (свободна)
H->>A: go Run(ctx, sid, "msg-1", msg)
Note over H,A: НЕ пишем в DB!
A->>A: validateUserMessage → append
A->>TUI: MessageReceived{msg-1, "привет"}
TUI->>TUI: рисует в чате
TUI->>TUI: проверяет pending по msg-1 (нет)
A->>TUI: TurnStart → StreamingToken → ...
Сценарий 2: сессия занята (например, работает сабагент)
sequenceDiagram
participant U as Пользователь
participant TUI
participant H as Handler
participant M as Manager
participant A as Agent.Run (работающий)
participant DB as SQLite
U->>TUI: печатает "ещё вопрос"
TUI->>H: SendMessage{msg-2, "ещё вопрос"}
Note over TUI: НЕ рисует!
H->>M: TryStart(sessionID)
M-->>H: false (занята)
H->>DB: AddPendingMessage(sid, msg-2)
H->>TUI: MessageQueued{msg-2, "ещё вопрос"}
TUI->>TUI: рисует в pending-очереди
Note over A: ...следующая итерация цикла...
A->>DB: drainPending
DB-->>A: [msg-2]
A->>TUI: MessageReceived{msg-2, "ещё вопрос"}
TUI->>TUI: рисует в чате
TUI->>TUI: убирает msg-2 из pending
Сценарий 3: sync tick (подхват pending из другой сессии)
sequenceDiagram
participant TUI
participant H as Handler
participant M as Manager
participant A as Agent.Run
participant DB as SQLite
Note over TUI: Sync tick каждые 2 сек
TUI->>DB: HasPendingMessages(sid)?
DB-->>TUI: true
TUI->>H: SendMessage{nil, ""}
H->>M: TryStart(sessionID)
M-->>H: true (свободна)
H->>A: go Run(ctx, sid, "", nil)
Note over A: nil message → только drainPending
A->>DB: drainPending
DB-->>A: [сообщения]
A->>TUI: MessageReceived{...} (для каждого)
Гарантии
- Нет дублей: если
TryStartуспешен — сообщение НЕ пишется в DB, передаётся напрямую вRun. Если занят — пишется в DB, не передаётся напрямую. - Нет зависаний: каждое сообщение в DB гарантированно дренажится через
drainPending(в началеRunи в каждой итерации цикла). - Race-free:
TryStart— атомарная проверка-and-set под mutex.