2026-10-15 18:33:18 -07:00
package agents
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
2026-10-16 12:56:53 -07:00
"sync"
2026-10-15 18:33:18 -07:00
2026-10-18 15:32:07 -07:00
"g.fc.run/ai/byre/internal/api"
"g.fc.run/ai/byre/internal/store"
2026-10-15 18:33:18 -07:00
)
2026-10-16 12:56:53 -07:00
type sessionLock struct {
mu sync . Mutex
refs int
}
func ( m * Manager ) lockDelivery ( sessionID string ) func () {
2026-10-15 18:33:18 -07:00
m . deliverMu . Lock ()
2026-10-16 12:56:53 -07:00
sl := m . deliverLocks [ sessionID ]
if sl == nil {
sl = & sessionLock {}
m . deliverLocks [ sessionID ] = sl
}
sl . refs ++
m . deliverMu . Unlock ()
sl . mu . Lock ()
return func () {
sl . mu . Unlock ()
m . deliverMu . Lock ()
sl . refs --
if sl . refs == 0 {
delete ( m . deliverLocks , sessionID )
}
m . deliverMu . Unlock ()
}
}
2026-10-20 23:15:19 -07:00
func ( m * Manager ) Message ( ctx context . Context , sessionID , actor , text , model , sendID , client string , images ... string ) ( api . AgentSession , bool , error ) {
2026-10-16 12:56:53 -07:00
unlock := m . lockDelivery ( sessionID )
defer unlock ()
2026-10-15 18:33:18 -07:00
2026-10-20 15:47:29 -07:00
prior , sent , err := m . priorSend ( ctx , sendID )
if err != nil || sent {
return prior , false , err
}
2026-10-15 18:33:18 -07:00
sess , err := m . store . GetSession ( ctx , sessionID )
if err != nil {
2026-10-18 00:12:42 -07:00
return api . AgentSession {}, false , err
2026-10-15 18:33:18 -07:00
}
2026-10-20 09:33:36 -07:00
if sess . State == api . AgentSessionStateIdle && isLeadActor ( actor ) {
err = m . checkNotStopped ( ctx , sess )
if err != nil {
return api . AgentSession {}, false , err
}
}
2026-10-20 11:01:59 -07:00
held := sess . State == api . AgentSessionStateIdle && sess . Role == api . Implementer && ( automatedActor ( actor ) && m . interrupted ( sess . Id ) || m . heldForLanding ( sess . TaskId , sess . Id ))
2026-10-19 14:08:56 -07:00
if ! held && ( sess . State != api . AgentSessionStateRunning || sess . Role != api . Implementer ) {
2026-10-20 23:15:19 -07:00
sess , err = m . Send ( ctx , sessionID , actor , text , model , sendID , client , images ... )
2026-10-18 00:12:42 -07:00
return sess , false , err
2026-10-15 18:33:18 -07:00
}
2026-10-20 22:53:47 -07:00
sess , err = m . changeModel ( ctx , sess , model , actor )
if err != nil {
return api . AgentSession {}, false , err
2026-10-15 18:33:18 -07:00
}
2026-10-17 15:33:59 -07:00
task , err := m . store . GetTask ( ctx , sess . TaskId )
if err != nil {
2026-10-18 00:12:42 -07:00
return api . AgentSession {}, false , err
2026-10-17 15:33:59 -07:00
}
2026-10-20 23:15:19 -07:00
err = m . store . AddTaskInput ( ctx , store . TaskInput { TaskID : sess . TaskId , Member : sess . Member , Actor : actor , Text : text , Images : images , SendID : sendID , Client : client }, task . ProjectId , sess . Id )
2026-10-15 18:33:18 -07:00
if err != nil {
2026-10-18 00:12:42 -07:00
return api . AgentSession {}, false , err
2026-10-15 18:33:18 -07:00
}
2026-10-19 14:08:56 -07:00
slog . Info ( "message queued" , "session" , sess . Id , "task" , sess . TaskId , "actor" , actor , "interrupted" , held )
2026-10-18 00:12:42 -07:00
return sess , true , nil
2026-10-15 18:33:18 -07:00
}
2026-10-20 15:47:29 -07:00
func ( m * Manager ) priorSend ( ctx context . Context , sendID string ) ( api . AgentSession , bool , error ) {
if sendID == "" {
return api . AgentSession {}, false , nil
}
sessionID , found , err := m . store . SendSession ( ctx , sendID )
if err != nil || ! found || sessionID == "" {
return api . AgentSession {}, found , err
}
sess , err := m . store . GetSession ( ctx , sessionID )
if err != nil {
return api . AgentSession {}, false , err
}
slog . Info ( "send already accepted" , "session" , sess . Id , "send" , sendID )
return sess , true , nil
}
2026-10-19 14:08:56 -07:00
func automatedActor ( actor string ) bool {
return actor == "byre" || actor == agentActor || actor == boardActor
}
func ( m * Manager ) interrupted ( sessionID string ) bool {
m . mu . Lock ()
defer m . mu . Unlock ()
l := m . live [ sessionID ]
return l != nil && l . interrupted
}
2026-10-17 15:45:27 -07:00
func ( m * Manager ) Withdraw ( ctx context . Context , taskID string , inputID int64 , actor string ) error {
task , err := m . store . GetTask ( ctx , taskID )
if err != nil {
return err
}
unlock := m . lockDelivery ( queueLockKey ( taskID ))
defer unlock ()
2026-10-18 08:48:57 -07:00
sessionID , err := m . store . WithdrawTaskInput ( ctx , taskID , inputID , task . ProjectId , actor )
2026-10-17 15:45:27 -07:00
if errors . Is ( err , store . ErrNotFound ) {
return fmt . Errorf ( "%w: message %d is not queued" , ErrConflict , inputID )
}
if err != nil {
return err
}
2026-10-18 08:48:57 -07:00
slog . Info ( "queued message withdrawn" , "session" , sessionID , "task" , taskID , "input" , inputID , "actor" , actor )
2026-10-17 15:45:27 -07:00
return nil
}
func queueLockKey ( taskID string ) string {
return "task:" + taskID
}
2026-10-15 18:33:18 -07:00
func ( m * Manager ) deliverQueued ( ctx context . Context , sess api . AgentSession ) ( bool , error ) {
2026-10-17 15:45:27 -07:00
unlock := m . lockDelivery ( queueLockKey ( sess . TaskId ))
defer unlock ()
2026-10-18 08:48:57 -07:00
inputs , err := m . store . TaskInputs ( ctx , sess . TaskId , sess . Member )
2026-10-15 18:33:18 -07:00
if err != nil || len ( inputs ) == 0 {
return false , err
}
2026-10-18 08:48:57 -07:00
if sess . Member != "" {
started , err := m . store . SessionStarted ( ctx , sess . Id )
if err != nil || ! started {
return false , err
}
}
2026-10-15 18:33:18 -07:00
task , err := m . store . GetTask ( ctx , sess . TaskId )
if err != nil {
return false , err
}
in := inputs [ 0 ]
2026-10-20 23:15:19 -07:00
_ , err = m . Send ( ctx , sess . Id , in . Actor , in . Text , "" , in . SendID , in . Client , in . Images ... )
2026-10-15 18:33:18 -07:00
if errors . Is ( err , ErrConflict ) {
slog . Info ( "queued message held: session changed state" , "session" , sess . Id , "task" , task . Id , "err" , err )
return true , nil
}
if err != nil {
return false , err
}
slog . Info ( "queued message sent" , "session" , sess . Id , "task" , task . Id , "input" , in . ID )
return true , m . store . DeleteTaskInputs ( ctx , [] int64 { in . ID })
}
func ( m * Manager ) deliverAllQueued ( ctx context . Context ) {
idle , err := m . store . SessionsInState ( ctx , api . AgentSessionStateIdle )
if err != nil {
slog . Error ( "list idle sessions" , "err" , err )
return
}
for _ , sess := range idle {
if sess . Role != api . Implementer {
continue
}
2026-10-16 12:56:53 -07:00
unlock := m . lockDelivery ( sess . Id )
2026-10-15 18:33:18 -07:00
_ , err = m . deliverQueued ( ctx , sess )
2026-10-16 12:56:53 -07:00
unlock ()
2026-10-15 18:33:18 -07:00
if err != nil {
slog . Error ( "deliver queued message" , "session" , sess . Id , "err" , err )
}
}
}
type queued struct {
2026-10-20 15:22:50 -07:00
actor string
text string
sendID string
2026-10-20 23:15:19 -07:00
client string
2026-10-15 18:33:18 -07:00
}
2026-10-16 14:18:40 -07:00
func ( m * Manager ) takeQueuedMidTurn ( ctx context . Context , l * live ) [] store . TaskInput {
2026-10-15 18:33:18 -07:00
m . mu . Lock ()
compacting := l . compacting
m . mu . Unlock ()
if compacting {
return nil
}
2026-10-17 15:45:27 -07:00
unlock := m . lockDelivery ( queueLockKey ( l . taskID ))
defer unlock ()
2026-10-18 08:48:57 -07:00
inputs , err := m . store . TaskInputs ( ctx , l . taskID , l . member )
2026-10-15 18:33:18 -07:00
if err != nil {
slog . Error ( "list queued messages" , "session" , l . id , "err" , err )
return nil
}
2026-10-16 14:18:40 -07:00
out := [] store . TaskInput {}
2026-10-15 18:33:18 -07:00
ids := [] int64 {}
for _ , in := range inputs {
if len ( in . Images ) > 0 {
break
}
2026-10-16 14:18:40 -07:00
out = append ( out , in )
2026-10-15 18:33:18 -07:00
ids = append ( ids , in . ID )
}
if len ( ids ) == 0 {
return nil
}
err = m . store . DeleteTaskInputs ( ctx , ids )
if err != nil {
slog . Error ( "delete queued messages" , "session" , l . id , "err" , err )
return nil
}
return out
}
2026-10-16 14:18:40 -07:00
func ( m * Manager ) takeLeadQueuedMidTurn ( ctx context . Context , l * live ) [] store . LeadInput {
2026-10-15 18:33:18 -07:00
m . mu . Lock ()
compacting := l . compacting
m . mu . Unlock ()
if compacting {
return nil
}
m . leadMu . Lock ()
defer m . leadMu . Unlock ()
inputs , err := m . store . LeadInputs ( ctx , l . owner )
if err != nil {
slog . Error ( "list held lead messages" , "session" , l . id , "err" , err )
return nil
}
2026-10-16 14:18:40 -07:00
out := [] store . LeadInput {}
2026-10-15 18:33:18 -07:00
ids := [] int64 {}
for _ , in := range inputs {
if in . Kind != store . LeadMessage {
continue
}
if len ( in . Images ) > 0 {
break
}
2026-10-16 14:18:40 -07:00
out = append ( out , in )
2026-10-15 18:33:18 -07:00
ids = append ( ids , in . ID )
}
if len ( ids ) == 0 {
return nil
}
err = m . store . DeleteLeadInputs ( ctx , ids )
if err != nil {
slog . Error ( "delete held lead messages" , "session" , l . id , "err" , err )
return nil
}
return out
}
func ( m * Manager ) recordQueued ( ctx context . Context , l * live , msgs [] queued ) string {
b := & strings . Builder {}
for i , q := range msgs {
2026-10-18 08:48:57 -07:00
if l . implementer () && isMergeInPrompt ( q . actor , q . text ) {
m . takeMergeTurn ( l )
}
2026-10-20 13:04:17 -07:00
payload := map [ string ] any { "text" : q . text , "mid_turn" : true }
if isFindingsMessage ( q . actor , q . text ) {
payload [ "findings" ] = true
}
2026-10-20 15:22:50 -07:00
if q . sendID != "" {
payload [ "send_id" ] = q . sendID
}
2026-10-20 23:15:19 -07:00
if q . client != "" {
payload [ "client" ] = q . client
}
2026-10-20 13:04:17 -07:00
err := m . emit ( ctx , l , q . actor , "user.message" , payload )
2026-10-15 18:33:18 -07:00
if err != nil {
slog . Error ( "record mid-turn message" , "session" , l . id , "err" , err )
}
if i > 0 {
b . WriteString ( "\n\n" )
}
2026-10-18 08:48:57 -07:00
if q . actor == boardActor {
fmt . Fprintf ( b , "While you were working, %s\n\nTake it into account now." , q . text )
continue
}
2026-10-20 12:20:51 -07:00
if isLeadActor ( q . actor ) {
fmt . Fprintf ( b , "%s (not %s) sent you this message while you were working; take it into account now:\n\n%s" , actorName ( q . actor ), leadOwner ( q . actor ), q . text )
continue
}
2026-10-20 23:15:19 -07:00
if q . client != "" {
fmt . Fprintf ( b , "%s sent you this message from %s while you were working; take it into account now:\n\n%s" , q . actor , m . clientName ( q . client ), q . text )
continue
}
2026-10-15 18:33:18 -07:00
fmt . Fprintf ( b , "%s sent you this message while you were working; take it into account now:\n\n%s" , q . actor , q . text )
}
slog . Info ( "queued messages sent mid-turn" , "session" , l . id , "count" , len ( msgs ))
return b . String ()
}