Files
tutti/packages/agent/daemon/runtime/controller_turn_completion.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

148 lines
4.4 KiB
Go

package agentruntime
import (
"context"
"log/slog"
"strings"
"time"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
)
const (
canonicalTerminalCommitRetryInitialBackoff = 100 * time.Millisecond
canonicalTerminalCommitRetryMaxBackoff = 5 * time.Second
)
type rootTurnCompletionAuthority uint8
const (
rootTurnCompletionAwaitProviderAggregation rootTurnCompletionAuthority = iota
rootTurnCompletionDirectCanonical
)
// classifyRootTurnCompletion distinguishes a terminal fact for the exact
// canonical Turn from provider-root lifecycle facts. Dispatch disposition is
// deliberately absent: admission and completion are independent contracts.
func classifyRootTurnCompletion(
turnID string,
events []activityshared.Event,
) rootTurnCompletionAuthority {
if turnHasTerminalEvent(events, turnID) {
return rootTurnCompletionDirectCanonical
}
return rootTurnCompletionAwaitProviderAggregation
}
// pendingCanonicalTerminalCommit retains a terminal fact that the Controller
// accepted in memory but could not yet confirm through the durable reporter.
// The owning blocking-exec goroutine keeps the exact active-turn fence until
// this record commits or daemon reconciliation proves the Turn already settled.
type pendingCanonicalTerminalCommit struct {
session Session
events []activityshared.Event
}
func (pending *pendingCanonicalTerminalCommit) merge(
session Session,
events []activityshared.Event,
) {
if pending == nil || len(events) == 0 {
return
}
pending.session = session
pending.events = append(
pending.events,
unemittedActivityEvents(events, pending.events)...,
)
}
func (pending *pendingCanonicalTerminalCommit) directCanonical(turnID string) bool {
return pending != nil && classifyRootTurnCompletion(
turnID,
pending.events,
) == rootTurnCompletionDirectCanonical
}
// convergeCanonicalTerminalCommit retries the same idempotent terminal facts
// after the bounded synchronous barrier fails. It intentionally outlives the
// submit caller's context: the submitted Turn is already durable, so only a
// successful terminal commit or an exact daemon reconciliation may release the
// active-turn fence.
func (c *Controller) convergeCanonicalTerminalCommit(
ctx context.Context,
turnID string,
pending *pendingCanonicalTerminalCommit,
) bool {
if c == nil || pending == nil || len(pending.events) == 0 {
return false
}
retryCtx := context.WithoutCancel(ctx)
backoff := canonicalTerminalCommitRetryInitialBackoff
attempt := 1
for {
timer := time.NewTimer(backoff)
<-timer.C
active, ok := c.activeTurn(
pending.session.RoomID,
pending.session.AgentSessionID,
)
if !ok || strings.TrimSpace(active.turnID) != strings.TrimSpace(turnID) {
// ReconcileRootTurnSettlement may win this race after proving the
// canonical Store is already settled. Never republish after that.
return false
}
session := c.preserveCurrentSessionSettings(pending.session)
accepted, ok := c.storeTurnSession(session, turnID)
if !ok {
return false
}
pending.session = accepted
err := c.reportSessionBeforePublish(retryCtx, accepted, pending.events)
if err == nil {
if !c.isProvisionalSession(accepted) {
c.publish(accepted, pending.events)
}
return pending.directCanonical(turnID)
}
slog.Warn(
"agent session terminal activity report remains pending",
"event", "agent_session.activity_report.terminal_commit_retry",
"room_id", accepted.RoomID,
"agent_session_id", accepted.AgentSessionID,
"turn_id", strings.TrimSpace(turnID),
"attempt", attempt,
"retry_backoff", backoff,
"error", err,
)
if backoff < canonicalTerminalCommitRetryMaxBackoff {
backoff *= 2
if backoff > canonicalTerminalCommitRetryMaxBackoff {
backoff = canonicalTerminalCommitRetryMaxBackoff
}
}
attempt++
}
}
// finalizeBlockingExecTurn owns the active-turn fence transition after a
// blocking adapter invocation exits. Root-provider adapters normally wait for
// canonical provider aggregation. The only providerless exception is an exact
// canonical terminal that already crossed the synchronous durable-report
// barrier; it needs no provider identity to become authoritative.
func (c *Controller) finalizeBlockingExecTurn(
session Session,
turnID string,
rootProviderLifecycle bool,
directCanonicalTerminalCommitted bool,
) {
if !rootProviderLifecycle || directCanonicalTerminalCommitted {
_, _ = c.finishTurn(session, turnID)
return
}
_, _ = c.storeTurnSession(session, turnID)
}