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

223 lines
7.5 KiB
Go

package agentruntime
import (
"fmt"
"log/slog"
"strings"
)
type codexAppServerTurnKind string
const (
codexAppServerTurnKindNormal codexAppServerTurnKind = "normal"
codexAppServerTurnKindCompact codexAppServerTurnKind = "compact"
// codexAppServerTurnKindGoalAdopted marks a turn that codex started on its
// own to continue an active goal, adopted by the reducer so its output is
// tracked like any Exec-driven turn.
codexAppServerTurnKindGoalAdopted codexAppServerTurnKind = "goal-adopted"
)
type codexAppServerTurnPhase string
const (
codexAppServerTurnPhaseIdle codexAppServerTurnPhase = "idle"
codexAppServerTurnPhaseRunning codexAppServerTurnPhase = "running"
codexAppServerTurnPhaseCompacting codexAppServerTurnPhase = "compacting"
codexAppServerTurnPhaseInterrupting codexAppServerTurnPhase = "interrupting"
codexAppServerTurnPhaseCompleted codexAppServerTurnPhase = "completed"
codexAppServerTurnPhaseFailed codexAppServerTurnPhase = "failed"
codexAppServerTurnPhaseCanceled codexAppServerTurnPhase = "canceled"
)
type codexAppServerTurnTerminal struct {
turn map[string]any
err error
phase codexAppServerTurnPhase
}
func (phase codexAppServerTurnPhase) terminal() bool {
switch phase {
case codexAppServerTurnPhaseCompleted,
codexAppServerTurnPhaseFailed,
codexAppServerTurnPhaseCanceled:
return true
default:
return false
}
}
func (a *CodexAppServerAdapter) transitionActiveTurnPhase(
agentSessionID string,
turn *codexAppServerActiveTurn,
phase codexAppServerTurnPhase,
) {
if a == nil || turn == nil || phase == "" {
return
}
a.mu.Lock()
defer a.mu.Unlock()
appSession := a.sessions[strings.TrimSpace(agentSessionID)]
if appSession == nil || appSession.activeTurn != turn || turn.phase.terminal() {
return
}
if phase == codexAppServerTurnPhaseCompacting {
turn.kind = codexAppServerTurnKindCompact
}
turn.phase = phase
}
// completeActiveTurn transitions the reducer-owned turn projection to a
// terminal phase after the `turn/completed` notification or an already-terminal
// initial turn snapshot. The blocking Exec wrapper observes the terminal
// channel; it no longer owns terminal classification.
func (a *CodexAppServerAdapter) completeActiveTurn(agentSessionID string, turn map[string]any) {
a.settleActiveTurn(agentSessionID, asString(turn["id"]), func(activeTurn *codexAppServerActiveTurn) codexAppServerTurnTerminal {
phase := appServerProjectedTurnTerminalPhase(turn, activeTurn.forceCanceled)
appServerLogTurnTerminalShadowMismatch(agentSessionID, turn, phase)
return codexAppServerTurnTerminal{turn: turn, phase: phase}
})
}
func (a *CodexAppServerAdapter) failActiveTurnFromAppServerError(agentSessionID string, params map[string]any) {
err := appServerNotificationError(params)
a.settleActiveTurn(agentSessionID, asString(params["turnId"]), func(*codexAppServerActiveTurn) codexAppServerTurnTerminal {
return codexAppServerTurnTerminal{err: err, phase: codexAppServerTurnPhaseFailed}
})
}
// settleActiveTurn owns the shared settle sequence — lock, session lookup,
// provider-turn-id match, phase transition, activeTurnID clear, non-blocking
// terminal send — so the completion and failure paths cannot diverge on the
// guards. terminalFor runs under the adapter lock with the matched active turn.
//
// A mismatched non-empty provider turn id settles anyway when the recorded id
// was never confirmed by turn/started: a turn/start issued while another turn
// is already running responds with a stub id that codex never starts — the
// input is steered into the running turn, so that turn's same-thread terminal
// is this turn's terminal (live-verified against codex 0.142.5,
// TestLiveProtocolTurnStartDuringActiveTurn). Requiring an exact match there
// wedged awaitTurnCompletion forever. Goal-adopted turns are the only active
// turns allowed to settle from an empty terminal id because their ownership was
// established by the preceding turn/started notification.
func (a *CodexAppServerAdapter) settleActiveTurn(
agentSessionID string,
providerTurnID string,
terminalFor func(*codexAppServerActiveTurn) codexAppServerTurnTerminal,
) {
if a == nil {
return
}
var activeTurn *codexAppServerActiveTurn
var terminal codexAppServerTurnTerminal
steerAdoptedTurnID := ""
droppedTurnID := ""
a.mu.Lock()
appSession := a.sessions[strings.TrimSpace(agentSessionID)]
if appSession != nil {
activeTurn = appSession.activeTurn
if activeTurn != nil {
expected := strings.TrimSpace(appSession.activeTurnID)
actual := strings.TrimSpace(providerTurnID)
switch {
case expected == "" || actual == expected:
case actual == "" && activeTurn.kind == codexAppServerTurnKindGoalAdopted:
case actual != "" && !appSession.activeTurnStartConfirmed:
steerAdoptedTurnID = expected
default:
droppedTurnID = expected
activeTurn = nil
}
}
if activeTurn != nil {
terminal = terminalFor(activeTurn)
activeTurn.phase = terminal.phase
appSession.activeTurnID = ""
appSession.activeTurnStartConfirmed = false
}
}
emits := activeTurn != nil && activeTurn.settleEmits
a.mu.Unlock()
if droppedTurnID != "" {
slog.Warn("agent session app-server turn terminal dropped on provider turn id mismatch",
"event", "agent_session.app_server.turn.terminal_id_mismatch",
"agent_session_id", agentSessionID,
"recorded_turn_id", droppedTurnID,
"terminal_turn_id", providerTurnID,
)
}
if activeTurn == nil {
return
}
if steerAdoptedTurnID != "" {
slog.Warn("agent session app-server steered turn adopted running turn terminal",
"event", "agent_session.app_server.turn.steer_terminal_adopted",
"agent_session_id", agentSessionID,
"stub_turn_id", steerAdoptedTurnID,
"terminal_turn_id", providerTurnID,
)
}
select {
case activeTurn.terminal <- terminal:
default:
}
if emits {
a.finalizeSettledTurn(agentSessionID, activeTurn, terminal)
}
}
func appServerProjectedTurnTerminalPhase(turn map[string]any, forceCanceled bool) codexAppServerTurnPhase {
if forceCanceled {
return codexAppServerTurnPhaseCanceled
}
switch strings.TrimSpace(asString(turn["status"])) {
case "failed":
return codexAppServerTurnPhaseFailed
case "interrupted", "canceled":
return codexAppServerTurnPhaseCanceled
default:
return codexAppServerTurnPhaseCompleted
}
}
func appServerLegacyTurnTerminalPhase(turn map[string]any) codexAppServerTurnPhase {
switch strings.TrimSpace(asString(turn["status"])) {
case "failed":
return codexAppServerTurnPhaseFailed
case "interrupted", "canceled":
return codexAppServerTurnPhaseCanceled
default:
return codexAppServerTurnPhaseCompleted
}
}
func appServerLogTurnTerminalShadowMismatch(
agentSessionID string,
turn map[string]any,
projected codexAppServerTurnPhase,
) {
legacy := appServerLegacyTurnTerminalPhase(turn)
if projected == legacy {
return
}
slog.Warn("agent session app-server turn projection terminal mismatch",
"event", "agent_session.app_server.turn_projection.shadow_mismatch",
"agent_session_id", agentSessionID,
"provider_turn_id", asString(turn["id"]),
"status", asString(turn["status"]),
"projected_phase", string(projected),
"legacy_phase", string(legacy),
)
}
func appServerNotificationError(params map[string]any) error {
turnError := payloadObject(params["error"])
message := strings.TrimSpace(asStringRaw(turnError["message"]))
if message == "" {
message = strings.TrimSpace(asStringRaw(params["message"]))
}
if message == "" {
return fmt.Errorf("codex app-server turn failed")
}
return fmt.Errorf("%s", message)
}