Files
tutti/services/tuttid/service/cli/providers/agentcontext/extension_availability_cache.go
T
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

106 lines
2.8 KiB
Go

package agentcontext
import (
"context"
"strings"
"sync"
"time"
agentextensionservice "github.com/tutti-os/tutti/services/tuttid/service/agentextension"
"golang.org/x/sync/singleflight"
)
const (
extensionAvailabilityCacheTTL = 30 * time.Second
extensionSetupProbeTimeout = 25 * time.Second
)
type extensionAvailabilityCacheEntry struct {
cachedAt time.Time
snapshot agentextensionservice.SetupSnapshot
}
// extensionAvailabilityCache coalesces target-scoped setup probes across both
// current exact-target callers and legacy broad-catalog callers in the daemon.
// The bounded TTL keeps sequential app startups from repeating authentication
// while explicit refreshes bypass it and probes remain independently bounded.
type extensionAvailabilityCache struct {
reader AgentTargetSetupReader
mu sync.RWMutex
entries map[string]extensionAvailabilityCacheEntry
group singleflight.Group
}
func newExtensionAvailabilityCache(reader AgentTargetSetupReader) *extensionAvailabilityCache {
if reader == nil {
return nil
}
return &extensionAvailabilityCache{
reader: reader,
entries: make(map[string]extensionAvailabilityCacheEntry),
}
}
func (c *extensionAvailabilityCache) load(
ctx context.Context,
input agentextensionservice.InstallPlanInput,
refresh bool,
) (agentextensionservice.SetupSnapshot, error) {
if c == nil || c.reader == nil {
return agentextensionservice.SetupSnapshot{}, context.Canceled
}
key := strings.TrimSpace(input.WorkspaceID) + "\x00" + strings.TrimSpace(input.AgentTargetID)
if !refresh {
if snapshot, ok := c.get(key, time.Now()); ok {
return snapshot, nil
}
}
result := c.group.DoChan(key, func() (any, error) {
if !refresh {
if snapshot, ok := c.get(key, time.Now()); ok {
return snapshot, nil
}
}
probeCtx, cancel := context.WithTimeout(context.Background(), extensionSetupProbeTimeout)
defer cancel()
snapshot, err := c.reader.GetSetup(probeCtx, input)
if err == nil {
c.set(key, time.Now(), snapshot)
}
return snapshot, err
})
select {
case <-ctx.Done():
return agentextensionservice.SetupSnapshot{}, ctx.Err()
case loaded := <-result:
if loaded.Err != nil {
return agentextensionservice.SetupSnapshot{}, loaded.Err
}
return loaded.Val.(agentextensionservice.SetupSnapshot), nil
}
}
func (c *extensionAvailabilityCache) get(
key string,
now time.Time,
) (agentextensionservice.SetupSnapshot, bool) {
c.mu.RLock()
entry, ok := c.entries[key]
c.mu.RUnlock()
if !ok || now.Sub(entry.cachedAt) > extensionAvailabilityCacheTTL {
return agentextensionservice.SetupSnapshot{}, false
}
return entry.snapshot, true
}
func (c *extensionAvailabilityCache) set(
key string,
cachedAt time.Time,
snapshot agentextensionservice.SetupSnapshot,
) {
c.mu.Lock()
c.entries[key] = extensionAvailabilityCacheEntry{cachedAt: cachedAt, snapshot: snapshot}
c.mu.Unlock()
}