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
90 lines
2.5 KiB
Go
90 lines
2.5 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"strings"
|
|
|
|
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
|
|
)
|
|
|
|
type RootTurnSettlement struct {
|
|
RoomID string
|
|
AgentSessionID string
|
|
TurnID string
|
|
Outcome string
|
|
ErrorMessage string
|
|
}
|
|
|
|
// ReconcileRootTurnSettlement applies daemon-owned durable root completion to
|
|
// the runtime view. It does not report the event back to the daemon: the
|
|
// caller invokes it only after the atomic durable transition committed.
|
|
func (c *Controller) ReconcileRootTurnSettlement(settlement RootTurnSettlement) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
roomID := strings.TrimSpace(settlement.RoomID)
|
|
agentSessionID := strings.TrimSpace(settlement.AgentSessionID)
|
|
turnID := strings.TrimSpace(settlement.TurnID)
|
|
if roomID == "" || agentSessionID == "" || turnID == "" {
|
|
return
|
|
}
|
|
key := sessionKey(roomID, agentSessionID)
|
|
c.mu.Lock()
|
|
session, ok := c.sessions[key]
|
|
if !ok {
|
|
c.mu.Unlock()
|
|
return
|
|
}
|
|
if active, exists := c.turns[key]; exists && strings.TrimSpace(active.turnID) == turnID {
|
|
delete(c.turns, key)
|
|
}
|
|
outcome := strings.TrimSpace(settlement.Outcome)
|
|
session.TurnLifecycle = &TurnLifecycle{Phase: "settled", Outcome: stringPointer(outcome)}
|
|
session.SubmitAvailability = availableSubmitAvailability()
|
|
switch outcome {
|
|
case "failed":
|
|
session.Status = SessionStatusFailed
|
|
case "canceled", "interrupted":
|
|
session.Status = SessionStatusCanceled
|
|
default:
|
|
session.Status = SessionStatusReady
|
|
}
|
|
session.UpdatedAtUnixMS = unixMS(now())
|
|
c.sessions[key] = session
|
|
c.mu.Unlock()
|
|
|
|
eventType := EventTurnCompleted
|
|
switch outcome {
|
|
case "failed":
|
|
eventType = EventTurnFailed
|
|
case "canceled", "interrupted":
|
|
eventType = EventTurnCanceled
|
|
}
|
|
metadata := map[string]any{
|
|
"daemonRootTurnSettlement": true,
|
|
}
|
|
if eventType == EventTurnFailed {
|
|
// Carry the durable failure reason on the live event so the
|
|
// visible-error projection can classify and render it; an empty
|
|
// detail degrades to a generic card with no recoverable signal.
|
|
if message := strings.TrimSpace(settlement.ErrorMessage); message != "" {
|
|
metadata["error"] = message
|
|
metadata["errorMessage"] = message
|
|
}
|
|
}
|
|
event := newTurnActivityEvent(session, eventType, turnID, session.Status, "", "", metadata)
|
|
if event.Type != "" {
|
|
if event.Payload.TurnOutcome == "" {
|
|
event.Payload.TurnOutcome = outcome
|
|
}
|
|
c.publish(session, []activityshared.Event{event})
|
|
}
|
|
}
|
|
|
|
func stringPointer(value string) *string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return nil
|
|
}
|
|
return &value
|
|
}
|