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
190 lines
5.8 KiB
Go
190 lines
5.8 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type GoalGenerationFenceRequest struct {
|
|
RoomID string
|
|
AgentSessionID string
|
|
OperationID string
|
|
Revision int64
|
|
RepairEpoch int64
|
|
Reason string
|
|
RequireLive bool
|
|
}
|
|
|
|
type controllerGoalGenerationFenceRegistry struct {
|
|
fences map[goalOperationIdentity]GoalGenerationFenceInput
|
|
version uint64
|
|
appliedVersion uint64
|
|
appliedLive bool
|
|
}
|
|
|
|
// FenceGoalGeneration retains the exact admission rule at Controller scope as
|
|
// well as installing it into the current adapter connection. Idle provider
|
|
// release discards adapter memory, but not this registry; the next connection
|
|
// must reinstall the registry before it accepts a user operation.
|
|
func (c *Controller) FenceGoalGeneration(ctx context.Context, input GoalGenerationFenceRequest) error {
|
|
releaseLifecycleLock, err := c.acquireLifecycleLockContext(ctx, input.RoomID, input.AgentSessionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer releaseLifecycleLock()
|
|
session, adapter, err := c.sessionAndAdapter(input.RoomID, input.AgentSessionID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, ok := adapter.(GoalGenerationFencer); !ok {
|
|
return fmt.Errorf("agent provider does not support Goal generation fencing")
|
|
}
|
|
fence, err := normalizeGoalGenerationFenceInput(input)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
c.retainGoalGenerationFence(session, fence)
|
|
if input.RequireLive {
|
|
if probe, ok := adapter.(LiveSessionProbeAdapter); ok && !probe.HasLiveSession(session) {
|
|
return ErrSessionDisconnected
|
|
}
|
|
} else if err := c.ensureLiveAdapterSession(ctx, session, adapter); err != nil {
|
|
return err
|
|
}
|
|
if err := c.applyRetainedGoalGenerationFencesOrClose(ctx, session, adapter); err != nil {
|
|
return err
|
|
}
|
|
return c.flushSessionReports(ctx, session)
|
|
}
|
|
|
|
func normalizeGoalGenerationFenceInput(input GoalGenerationFenceRequest) (GoalGenerationFenceInput, error) {
|
|
return normalizeRetainedGoalGenerationFenceInput(GoalGenerationFenceInput{
|
|
OperationID: strings.TrimSpace(input.OperationID),
|
|
Revision: input.Revision,
|
|
RepairEpoch: input.RepairEpoch,
|
|
Reason: strings.TrimSpace(input.Reason),
|
|
})
|
|
}
|
|
|
|
func normalizeRetainedGoalGenerationFenceInput(input GoalGenerationFenceInput) (GoalGenerationFenceInput, error) {
|
|
fence := GoalGenerationFenceInput{
|
|
OperationID: strings.TrimSpace(input.OperationID),
|
|
Revision: input.Revision,
|
|
RepairEpoch: input.RepairEpoch,
|
|
Reason: strings.TrimSpace(input.Reason),
|
|
}
|
|
identity := goalOperationIdentity{
|
|
operationID: fence.OperationID,
|
|
revision: fence.Revision,
|
|
repairEpoch: fence.RepairEpoch,
|
|
}
|
|
if !identity.valid() {
|
|
return GoalGenerationFenceInput{}, errors.New("valid Goal generation fence identity is required")
|
|
}
|
|
return fence, nil
|
|
}
|
|
|
|
func (c *Controller) retainGoalGenerationFence(session Session, fence GoalGenerationFenceInput) {
|
|
key := sessionKey(session.RoomID, session.AgentSessionID)
|
|
identity := goalOperationIdentity{
|
|
operationID: fence.OperationID,
|
|
revision: fence.Revision,
|
|
repairEpoch: fence.RepairEpoch,
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.goalGenerationFences == nil {
|
|
c.goalGenerationFences = make(map[string]*controllerGoalGenerationFenceRegistry)
|
|
}
|
|
registry := c.goalGenerationFences[key]
|
|
if registry == nil {
|
|
registry = &controllerGoalGenerationFenceRegistry{
|
|
fences: make(map[goalOperationIdentity]GoalGenerationFenceInput),
|
|
}
|
|
c.goalGenerationFences[key] = registry
|
|
}
|
|
if existing, found := registry.fences[identity]; found && existing.Reason == fence.Reason {
|
|
return
|
|
}
|
|
registry.fences[identity] = fence
|
|
registry.version++
|
|
}
|
|
|
|
func (c *Controller) applyRetainedGoalGenerationFences(ctx context.Context, session Session, adapter Adapter) error {
|
|
fencer, ok := adapter.(GoalGenerationFencer)
|
|
key := sessionKey(session.RoomID, session.AgentSessionID)
|
|
c.mu.Lock()
|
|
registry := c.goalGenerationFences[key]
|
|
if registry == nil || len(registry.fences) == 0 {
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
if !ok {
|
|
c.mu.Unlock()
|
|
return fmt.Errorf("agent provider does not support Goal generation fencing")
|
|
}
|
|
if registry.appliedLive && registry.appliedVersion == registry.version {
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
version := registry.version
|
|
fences := make([]GoalGenerationFenceInput, 0, len(registry.fences))
|
|
for _, fence := range registry.fences {
|
|
fences = append(fences, fence)
|
|
}
|
|
c.mu.Unlock()
|
|
sort.Slice(fences, func(left, right int) bool {
|
|
if fences[left].Revision != fences[right].Revision {
|
|
return fences[left].Revision < fences[right].Revision
|
|
}
|
|
if fences[left].RepairEpoch != fences[right].RepairEpoch {
|
|
return fences[left].RepairEpoch < fences[right].RepairEpoch
|
|
}
|
|
return fences[left].OperationID < fences[right].OperationID
|
|
})
|
|
|
|
for _, fence := range fences {
|
|
if err := fencer.FenceGoalGeneration(ctx, session, fence); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
c.mu.Lock()
|
|
if current := c.goalGenerationFences[key]; current != nil && current.version == version {
|
|
current.appliedVersion = version
|
|
current.appliedLive = true
|
|
}
|
|
c.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) applyRetainedGoalGenerationFencesOrClose(
|
|
ctx context.Context,
|
|
session Session,
|
|
adapter Adapter,
|
|
) error {
|
|
if err := c.applyRetainedGoalGenerationFences(ctx, session, adapter); err != nil {
|
|
c.invalidateAppliedGoalGenerationFences(session)
|
|
return errors.Join(err, adapter.Close(context.WithoutCancel(ctx), session))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) invalidateAppliedGoalGenerationFences(session Session) {
|
|
key := sessionKey(session.RoomID, session.AgentSessionID)
|
|
c.mu.Lock()
|
|
if registry := c.goalGenerationFences[key]; registry != nil {
|
|
registry.appliedLive = false
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func (c *Controller) deleteRetainedGoalGenerationFences(roomID, agentSessionID string) {
|
|
key := sessionKey(roomID, agentSessionID)
|
|
c.mu.Lock()
|
|
delete(c.goalGenerationFences, key)
|
|
c.mu.Unlock()
|
|
}
|