源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package agentruntime
|
||||
|
||||
import "strings"
|
||||
|
||||
// Session commit boundary
|
||||
// -----------------------
|
||||
//
|
||||
// A turn-execution goroutine captures a Session snapshot when its turn begins
|
||||
// and folds provider events onto that local value while the provider runs.
|
||||
// Meanwhile the controller's live session may be mutated concurrently by other
|
||||
// lifecycle commands (SetTitle, SetVisible, UpdateSettings), so the exec-path
|
||||
// snapshot is never the accepted state. commitTurnExecSessionLocked is the
|
||||
// single boundary through which those goroutines write back, and it merges only
|
||||
// the fields turn execution owns. Callers must publish and report using the
|
||||
// returned accepted session, never the exec-path snapshot.
|
||||
|
||||
// mergeTurnExecSession produces the accepted session for a turn-execution
|
||||
// commit. `current` is the controller's current session (the only accepted
|
||||
// state); `update` is the turn goroutine's local snapshot. Only
|
||||
// turn-execution-owned fields are taken from `update`; user-mutated fields
|
||||
// (Title, Visible, Settings, PermissionModeID) and identity fields are always
|
||||
// preserved from `current`, so a concurrent SetTitle/SetVisible/UpdateSettings
|
||||
// can never be lost to a stale exec copy.
|
||||
func mergeTurnExecSession(current, update Session) (Session, bool) {
|
||||
// A stale adapter lifecycle snapshot (already superseded by a newer one
|
||||
// applied through the session-event sink) must never regress the accepted
|
||||
// session. This lost-update guard previously lived only in storeTurnSession;
|
||||
// finishTurn now applies the same rule so settlement cannot resurrect an
|
||||
// older lifecycle.
|
||||
if current.LifecycleAuthority && current.LifecycleSeq > update.LifecycleSeq {
|
||||
return Session{}, false
|
||||
}
|
||||
accepted := current
|
||||
|
||||
// Turn-execution-owned fields: lifecycle, status, availability, and the
|
||||
// event-derived facts only turn execution observes.
|
||||
accepted.Status = update.Status
|
||||
accepted.TurnLifecycle = cloneRuntimeTurnLifecycle(update.TurnLifecycle)
|
||||
accepted.SubmitAvailability = cloneRuntimeSubmitAvailability(update.SubmitAvailability)
|
||||
accepted.LastError = update.LastError
|
||||
accepted.Resumable = current.Resumable || update.Resumable
|
||||
if providerSessionID := strings.TrimSpace(update.ProviderSessionID); providerSessionID != "" {
|
||||
accepted.ProviderSessionID = providerSessionID
|
||||
}
|
||||
if update.UpdatedAtUnixMS > current.UpdatedAtUnixMS {
|
||||
accepted.UpdatedAtUnixMS = update.UpdatedAtUnixMS
|
||||
}
|
||||
if update.LifecycleAuthority {
|
||||
accepted.LifecycleAuthority = true
|
||||
if update.LifecycleSeq > accepted.LifecycleSeq {
|
||||
accepted.LifecycleSeq = update.LifecycleSeq
|
||||
}
|
||||
}
|
||||
|
||||
// Title ownership: a title the user set explicitly is immutable from the
|
||||
// exec path's perspective. A provider/event title is only a candidate while
|
||||
// no user title exists; once accepted it is carried into the accepted
|
||||
// session without changing the established-title state.
|
||||
if current.UserTitleSet {
|
||||
accepted.Title = current.Title
|
||||
} else if strings.TrimSpace(update.Title) != "" {
|
||||
accepted.Title = strings.TrimSpace(update.Title)
|
||||
}
|
||||
accepted.UserTitleSet = current.UserTitleSet
|
||||
|
||||
accepted.RuntimeContext = mergeTurnExecRuntimeContext(
|
||||
current.RuntimeContext,
|
||||
update.RuntimeContext,
|
||||
accepted.SettingsValue(),
|
||||
accepted.InitialTitleEstablished,
|
||||
)
|
||||
return accepted, true
|
||||
}
|
||||
|
||||
// mergeTurnExecRuntimeContext preserves the accepted runtime context while
|
||||
// merging event-derived keys from the exec snapshot, then re-asserts the
|
||||
// settings-derived keys and the initial-title marker from the accepted session
|
||||
// so a stale exec copy can never clobber concurrent settings or title state.
|
||||
func mergeTurnExecRuntimeContext(
|
||||
current, update map[string]any,
|
||||
settings SessionSettings,
|
||||
established bool,
|
||||
) map[string]any {
|
||||
merged := clonePayload(current)
|
||||
if merged == nil {
|
||||
merged = map[string]any{}
|
||||
}
|
||||
if len(update) > 0 {
|
||||
merged = mergeRuntimeContextPatch(merged, update)
|
||||
}
|
||||
merged = runtimeContextWithSessionSettings(merged, settings)
|
||||
return runtimeContextWithInitialTitleEstablished(merged, established)
|
||||
}
|
||||
|
||||
// commitTurnExecSessionLocked validates the turn fence, applies the merge onto
|
||||
// the controller's current session, optionally settles the turn, and persists
|
||||
// the accepted session. When settle is true the matched turn fence is always
|
||||
// removed — even if the merge rejects the write (different live turn, newer
|
||||
// adapter lifecycle, closed session) — because a stale exec snapshot must not
|
||||
// keep the fence open.
|
||||
func (c *Controller) commitTurnExecSessionLocked(key, turnID string, update Session, settle bool) (Session, bool) {
|
||||
active, ok := c.turns[key]
|
||||
if !ok || strings.TrimSpace(active.turnID) != strings.TrimSpace(turnID) {
|
||||
return Session{}, false
|
||||
}
|
||||
if settle {
|
||||
delete(c.turns, key)
|
||||
}
|
||||
current, ok := c.sessions[key]
|
||||
if !ok {
|
||||
return Session{}, false
|
||||
}
|
||||
if settle && sessionHasDifferentLiveTurn(current, turnID) {
|
||||
return Session{}, false
|
||||
}
|
||||
accepted, ok := mergeTurnExecSession(current, update)
|
||||
if !ok {
|
||||
return Session{}, false
|
||||
}
|
||||
if settle && !accepted.LifecycleAuthority {
|
||||
// ADR 0008: snapshot-authority sessions already settle through their own
|
||||
// copied lifecycle; legacy sessions get the controller-origin settle and
|
||||
// a pure status reconcile.
|
||||
accepted = settleFinishedTurnLifecycle(accepted, turnID)
|
||||
accepted = c.reconcileSessionStatusLocked(key, accepted)
|
||||
}
|
||||
c.sessions[key] = accepted
|
||||
return accepted, true
|
||||
}
|
||||
Reference in New Issue
Block a user