Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
77 lines
3.1 KiB
Go
77 lines
3.1 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"strings"
|
|
|
|
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
|
)
|
|
|
|
// storeTurnSession commits a mid-turn lifecycle snapshot produced by a turn
|
|
// execution goroutine through the unified commit boundary. The controller's
|
|
// current session is the only accepted state, so a concurrent user mutation
|
|
// (SetTitle/SetVisible/UpdateSettings) is never overwritten by the stale exec
|
|
// copy. It returns the accepted session; callers must publish and report with
|
|
// that value, never the exec-path snapshot.
|
|
func (c *Controller) storeTurnSession(session Session, turnID string) (Session, bool) {
|
|
if c == nil {
|
|
return Session{}, false
|
|
}
|
|
key := sessionKey(session.RoomID, session.AgentSessionID)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return c.commitTurnExecSessionLocked(key, turnID, session, false)
|
|
}
|
|
|
|
// finishTurn settles the turn owned by an execution goroutine through the
|
|
// unified commit boundary and returns the accepted session. Like
|
|
// storeTurnSession it merges only turn-execution-owned fields, so settlement
|
|
// never reverts a concurrent user title, visibility, or settings change.
|
|
func (c *Controller) finishTurn(session Session, turnID string) (Session, bool) {
|
|
if c == nil {
|
|
return Session{}, false
|
|
}
|
|
key := sessionKey(session.RoomID, session.AgentSessionID)
|
|
needsFallback := session.LifecycleAuthority &&
|
|
lifecycleStillActiveForTurn(session.TurnLifecycle, turnID)
|
|
c.mu.Lock()
|
|
accepted, ok := c.commitTurnExecSessionLocked(key, turnID, session, true)
|
|
c.mu.Unlock()
|
|
if !ok {
|
|
return Session{}, false
|
|
}
|
|
if needsFallback {
|
|
c.applySessionEventsByAgentSessionID(accepted.AgentSessionID, settledFallbackTurnEvents(accepted, turnID))
|
|
}
|
|
return accepted, true
|
|
}
|
|
|
|
// lifecycleStillActiveForTurn reports that the exec-path lifecycle still marks
|
|
// the given turn live after Exec returned. For snapshot-authority sessions that
|
|
// means the adapter never settled the turn it owns (for example a submission
|
|
// absorbed by steering), so the controller publishes its own settled snapshot
|
|
// to reach reporter/GUI.
|
|
func lifecycleStillActiveForTurn(lifecycle *TurnLifecycle, turnID string) bool {
|
|
if lifecycle == nil || lifecycle.ActiveTurnID == nil {
|
|
return false
|
|
}
|
|
return strings.TrimSpace(*lifecycle.ActiveTurnID) == strings.TrimSpace(turnID) &&
|
|
runtimeTurnLifecyclePhaseIsLive(lifecycle.Phase)
|
|
}
|
|
|
|
// settledFallbackTurnEvents builds the controller-origin settled snapshot the
|
|
// finishTurn fallback publishes when the adapter never settled the turn it
|
|
// owns (for example a submission absorbed by steering).
|
|
func settledFallbackTurnEvents(session Session, turnID string) []activityshared.Event {
|
|
ctx, ok := activityEventContext(session, "turn-settled:"+turnID, turnID)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
event := activityshared.NewTurnUpdated(ctx, turnID, activityshared.TurnPhaseIdle)
|
|
activityshared.StampTurnLifecycleSnapshot(&event, activityshared.TurnLifecycleSnapshot{
|
|
Origin: activityshared.TurnLifecycleOriginController,
|
|
Phase: "settled",
|
|
Outcome: string(activityshared.TurnOutcomeCompleted),
|
|
})
|
|
return []activityshared.Event{event}
|
|
}
|