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
206 lines
5.2 KiB
Go
206 lines
5.2 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
sessionreplay "github.com/tutti-os/tutti/packages/agent/session-replay"
|
|
)
|
|
|
|
var ErrReplayProviderOvershot = errors.New("checkpoint_provider_overshot")
|
|
|
|
// errReplaySyntheticPending tells the ACP read loop to drain pending synthetic
|
|
// optional-probe responses before retrying a checkpoint barrier wait.
|
|
var errReplaySyntheticPending = errors.New("replay synthetic response pending")
|
|
|
|
type replayProviderInputBarrier struct {
|
|
mu sync.Mutex
|
|
active bool
|
|
targets map[string]sessionreplay.ProviderUnitPosition
|
|
handled map[string]sessionreplay.ProviderUnitPosition
|
|
changed chan struct{}
|
|
}
|
|
|
|
func newReplayProviderInputBarrier() *replayProviderInputBarrier {
|
|
return &replayProviderInputBarrier{
|
|
targets: map[string]sessionreplay.ProviderUnitPosition{},
|
|
handled: map[string]sessionreplay.ProviderUnitPosition{},
|
|
changed: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
func (b *replayProviderInputBarrier) setTargets(
|
|
targets []sessionreplay.ProviderUnitPosition,
|
|
) error {
|
|
next := make(map[string]sessionreplay.ProviderUnitPosition, len(targets))
|
|
for _, target := range targets {
|
|
if target.ConnectionID == "" {
|
|
return errors.New("replay provider target connection is empty")
|
|
}
|
|
if _, duplicate := next[target.ConnectionID]; duplicate {
|
|
return fmt.Errorf("duplicate replay provider target %q", target.ConnectionID)
|
|
}
|
|
next[target.ConnectionID] = target
|
|
}
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
for connectionID, handled := range b.handled {
|
|
target, ok := next[connectionID]
|
|
if !ok || compareReplayProviderPosition(handled, target) > 0 {
|
|
return fmt.Errorf(
|
|
"%w: connection %s already handled %d/%d",
|
|
ErrReplayProviderOvershot,
|
|
connectionID,
|
|
handled.ChunkSeq,
|
|
handled.UnitIndex,
|
|
)
|
|
}
|
|
}
|
|
b.active = true
|
|
b.targets = next
|
|
b.signalChangedLocked()
|
|
return nil
|
|
}
|
|
|
|
func (b *replayProviderInputBarrier) clearTargets() {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.active = false
|
|
b.targets = map[string]sessionreplay.ProviderUnitPosition{}
|
|
b.signalChangedLocked()
|
|
}
|
|
|
|
func (b *replayProviderInputBarrier) complete(
|
|
ctx context.Context,
|
|
unit ProviderInputUnit,
|
|
closed <-chan struct{},
|
|
interrupt <-chan struct{},
|
|
) error {
|
|
for {
|
|
b.mu.Lock()
|
|
previous := b.handled[unit.Position.ConnectionID]
|
|
if compareReplayProviderPosition(unit.Position, previous) < 0 {
|
|
b.mu.Unlock()
|
|
return fmt.Errorf(
|
|
"%w: connection %s input position moved backward",
|
|
ErrReplayProviderOvershot,
|
|
unit.Position.ConnectionID,
|
|
)
|
|
}
|
|
if compareReplayProviderPosition(unit.Position, previous) > 0 {
|
|
b.handled[unit.Position.ConnectionID] = unit.Position
|
|
b.signalChangedLocked()
|
|
}
|
|
if !b.active {
|
|
b.mu.Unlock()
|
|
return nil
|
|
}
|
|
target, ok := b.targets[unit.Position.ConnectionID]
|
|
if !ok || compareReplayProviderPosition(unit.Position, target) > 0 {
|
|
b.mu.Unlock()
|
|
return fmt.Errorf(
|
|
"%w: connection %s handled %d/%d past target",
|
|
ErrReplayProviderOvershot,
|
|
unit.Position.ConnectionID,
|
|
unit.Position.ChunkSeq,
|
|
unit.Position.UnitIndex,
|
|
)
|
|
}
|
|
if compareReplayProviderPosition(unit.Position, target) < 0 {
|
|
b.mu.Unlock()
|
|
return nil
|
|
}
|
|
changed := b.changed
|
|
b.mu.Unlock()
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-closed:
|
|
return context.Canceled
|
|
case <-changed:
|
|
case <-interrupt:
|
|
return errReplaySyntheticPending
|
|
}
|
|
}
|
|
}
|
|
|
|
// waitForProgressDuration measures only time in which this connection can
|
|
// consume provider input. Checkpoint barriers deliberately stop that progress;
|
|
// wall-clock recovery timers must not expire while replay is parked there.
|
|
func (b *replayProviderInputBarrier) waitForProgressDuration(
|
|
ctx context.Context,
|
|
connectionID string,
|
|
duration time.Duration,
|
|
closed <-chan struct{},
|
|
) error {
|
|
remaining := duration
|
|
for remaining > 0 {
|
|
b.mu.Lock()
|
|
handled := b.handled[connectionID]
|
|
target, targeted := b.targets[connectionID]
|
|
blocked := b.active && targeted && compareReplayProviderPosition(handled, target) >= 0
|
|
changed := b.changed
|
|
b.mu.Unlock()
|
|
|
|
if blocked {
|
|
if err := waitForReplayPlaybackChange(ctx, closed, changed, nil); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
started := time.Now()
|
|
timer := time.NewTimer(remaining)
|
|
select {
|
|
case <-ctx.Done():
|
|
stopReplayPlaybackTimer(timer)
|
|
return ctx.Err()
|
|
case <-closed:
|
|
stopReplayPlaybackTimer(timer)
|
|
return context.Canceled
|
|
case <-changed:
|
|
stopReplayPlaybackTimer(timer)
|
|
remaining -= time.Since(started)
|
|
case <-timer.C:
|
|
return nil
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *replayProviderInputBarrier) state() map[string]sessionreplay.ProviderUnitPosition {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
result := make(map[string]sessionreplay.ProviderUnitPosition, len(b.handled))
|
|
for connectionID, position := range b.handled {
|
|
result[connectionID] = position
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (b *replayProviderInputBarrier) signalChangedLocked() {
|
|
close(b.changed)
|
|
b.changed = make(chan struct{})
|
|
}
|
|
|
|
func compareReplayProviderPosition(
|
|
left, right sessionreplay.ProviderUnitPosition,
|
|
) int {
|
|
if left.ChunkSeq < right.ChunkSeq {
|
|
return -1
|
|
}
|
|
if left.ChunkSeq > right.ChunkSeq {
|
|
return 1
|
|
}
|
|
if left.UnitIndex < right.UnitIndex {
|
|
return -1
|
|
}
|
|
if left.UnitIndex > right.UnitIndex {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|