Files
tutti/packages/agent/daemon/runtime/controller_turn_finish.go
agent-radar 2ed3415fb7
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
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

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}
}