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
346 lines
9.0 KiB
Go
346 lines
9.0 KiB
Go
package agentstatus
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"golang.org/x/sync/singleflight"
|
|
)
|
|
|
|
type executableFingerprint struct {
|
|
info os.FileInfo
|
|
resolvedPath string
|
|
}
|
|
|
|
func readExecutableFingerprint(path string) (executableFingerprint, bool) {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return executableFingerprint{}, false
|
|
}
|
|
resolvedPath, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
resolvedPath = filepath.Clean(path)
|
|
}
|
|
info, err := os.Stat(resolvedPath)
|
|
if err != nil {
|
|
return executableFingerprint{}, false
|
|
}
|
|
return executableFingerprint{
|
|
info: info,
|
|
resolvedPath: filepath.Clean(resolvedPath),
|
|
}, true
|
|
}
|
|
|
|
func sameExecutableFingerprint(left executableFingerprint, right executableFingerprint) bool {
|
|
return left.resolvedPath == right.resolvedPath &&
|
|
left.info.Size() == right.info.Size() &&
|
|
left.info.ModTime().Equal(right.info.ModTime()) &&
|
|
os.SameFile(left.info, right.info)
|
|
}
|
|
|
|
type cliVersionCacheEntry struct {
|
|
fingerprint executableFingerprint
|
|
output string
|
|
}
|
|
|
|
// CLIVersionCache reuses successful `--version` output until the resolved
|
|
// executable changes. A forced auth/readiness refresh does not need to restart
|
|
// an unchanged binary just to rediscover the same version.
|
|
type CLIVersionCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]cliVersionCacheEntry
|
|
group singleflight.Group
|
|
}
|
|
|
|
func NewCLIVersionCache() *CLIVersionCache {
|
|
return &CLIVersionCache{entries: make(map[string]cliVersionCacheEntry)}
|
|
}
|
|
|
|
func (c *CLIVersionCache) load(binaryPath string, loader func() string) string {
|
|
if c == nil {
|
|
return loader()
|
|
}
|
|
key := filepath.Clean(strings.TrimSpace(binaryPath))
|
|
if output, ok := c.get(key); ok {
|
|
return output
|
|
}
|
|
value, _, _ := c.group.Do(key, func() (any, error) {
|
|
if output, ok := c.get(key); ok {
|
|
return output, nil
|
|
}
|
|
output := loader()
|
|
if output != "" {
|
|
c.set(key, output)
|
|
}
|
|
return output, nil
|
|
})
|
|
return value.(string)
|
|
}
|
|
|
|
func (c *CLIVersionCache) get(binaryPath string) (string, bool) {
|
|
fingerprint, ok := readExecutableFingerprint(binaryPath)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
c.mu.RLock()
|
|
entry, found := c.entries[binaryPath]
|
|
c.mu.RUnlock()
|
|
if !found || !sameExecutableFingerprint(entry.fingerprint, fingerprint) {
|
|
return "", false
|
|
}
|
|
return entry.output, true
|
|
}
|
|
|
|
func (c *CLIVersionCache) set(binaryPath string, output string) {
|
|
fingerprint, ok := readExecutableFingerprint(binaryPath)
|
|
if !ok {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
c.entries[binaryPath] = cliVersionCacheEntry{
|
|
fingerprint: fingerprint,
|
|
output: output,
|
|
}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
type adapterProbeCacheEntry struct {
|
|
fingerprint executableFingerprint
|
|
checkedAt time.Time
|
|
}
|
|
|
|
// AdapterProbeCache stores only successful launch probes. Failures are always
|
|
// retried, and explicit refresh/probe paths bypass this cache.
|
|
type AdapterProbeCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]adapterProbeCacheEntry
|
|
}
|
|
|
|
type bunGlobalBinCacheEntry struct {
|
|
fingerprint executableFingerprint
|
|
binDir string
|
|
}
|
|
|
|
type globalBinDiscoveryCacheEntry struct {
|
|
fingerprint executableFingerprint
|
|
binDir string
|
|
}
|
|
|
|
// BunGlobalBinCache caches only successful `bun pm bin -g` discoveries until
|
|
// the resolved Bun executable changes. Failures are retried so installing Bun
|
|
// or correcting bunfig.toml does not require a daemon restart.
|
|
type BunGlobalBinCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]bunGlobalBinCacheEntry
|
|
group singleflight.Group
|
|
}
|
|
|
|
func NewBunGlobalBinCache() *BunGlobalBinCache {
|
|
return &BunGlobalBinCache{entries: make(map[string]bunGlobalBinCacheEntry)}
|
|
}
|
|
|
|
func (c *BunGlobalBinCache) load(bunPath string, loader func() string) string {
|
|
if c == nil {
|
|
return loader()
|
|
}
|
|
key := filepath.Clean(strings.TrimSpace(bunPath))
|
|
if binDir, ok := c.get(key); ok {
|
|
return binDir
|
|
}
|
|
value, _, _ := c.group.Do(key, func() (any, error) {
|
|
if binDir, ok := c.get(key); ok {
|
|
return binDir, nil
|
|
}
|
|
binDir := loader()
|
|
if binDir != "" {
|
|
c.set(key, binDir)
|
|
}
|
|
return binDir, nil
|
|
})
|
|
return value.(string)
|
|
}
|
|
|
|
func (c *BunGlobalBinCache) get(bunPath string) (string, bool) {
|
|
fingerprint, ok := readExecutableFingerprint(bunPath)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
c.mu.RLock()
|
|
entry, found := c.entries[bunPath]
|
|
c.mu.RUnlock()
|
|
if !found || !sameExecutableFingerprint(entry.fingerprint, fingerprint) {
|
|
return "", false
|
|
}
|
|
return entry.binDir, true
|
|
}
|
|
|
|
func (c *BunGlobalBinCache) set(bunPath string, binDir string) {
|
|
fingerprint, ok := readExecutableFingerprint(bunPath)
|
|
if !ok {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
c.entries[bunPath] = bunGlobalBinCacheEntry{fingerprint: fingerprint, binDir: binDir}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// GlobalBinDiscoveryCache caches successful global-bin discovery commands
|
|
// such as `pnpm bin -g`, `npm prefix -g`, and `brew --prefix`. Entries are
|
|
// invalidated whenever their manager executable changes; failed queries are
|
|
// intentionally not cached so a newly-installed manager is visible without a
|
|
// daemon restart.
|
|
type GlobalBinDiscoveryCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]globalBinDiscoveryCacheEntry
|
|
group singleflight.Group
|
|
}
|
|
|
|
func NewGlobalBinDiscoveryCache() *GlobalBinDiscoveryCache {
|
|
return &GlobalBinDiscoveryCache{entries: make(map[string]globalBinDiscoveryCacheEntry)}
|
|
}
|
|
|
|
func (c *GlobalBinDiscoveryCache) load(managerPath, queryKey string, loader func() string) string {
|
|
if c == nil {
|
|
return loader()
|
|
}
|
|
key := globalBinDiscoveryCacheKey(managerPath, queryKey)
|
|
if binDir, ok := c.get(key, managerPath); ok {
|
|
return binDir
|
|
}
|
|
value, _, _ := c.group.Do(key, func() (any, error) {
|
|
if binDir, ok := c.get(key, managerPath); ok {
|
|
return binDir, nil
|
|
}
|
|
binDir := loader()
|
|
if binDir != "" {
|
|
c.set(key, managerPath, binDir)
|
|
}
|
|
return binDir, nil
|
|
})
|
|
return value.(string)
|
|
}
|
|
|
|
func (c *GlobalBinDiscoveryCache) get(key, managerPath string) (string, bool) {
|
|
fingerprint, ok := readExecutableFingerprint(managerPath)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
c.mu.RLock()
|
|
entry, found := c.entries[key]
|
|
c.mu.RUnlock()
|
|
if !found || !sameExecutableFingerprint(entry.fingerprint, fingerprint) {
|
|
return "", false
|
|
}
|
|
return entry.binDir, true
|
|
}
|
|
|
|
func (c *GlobalBinDiscoveryCache) set(key, managerPath, binDir string) {
|
|
fingerprint, ok := readExecutableFingerprint(managerPath)
|
|
if !ok {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
c.entries[key] = globalBinDiscoveryCacheEntry{fingerprint: fingerprint, binDir: binDir}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
func globalBinDiscoveryCacheKey(managerPath, queryKey string) string {
|
|
return filepath.Clean(strings.TrimSpace(managerPath)) + "\x00" + strings.TrimSpace(queryKey)
|
|
}
|
|
|
|
func NewAdapterProbeCache() *AdapterProbeCache {
|
|
return &AdapterProbeCache{entries: make(map[string]adapterProbeCacheEntry)}
|
|
}
|
|
|
|
func (c *AdapterProbeCache) ready(key string, binaryPath string) bool {
|
|
return c.readyWithin(key, binaryPath, time.Now(), 0)
|
|
}
|
|
|
|
// readyWithin accepts only a prior successful protocol handshake whose
|
|
// executable identity and freshness window still match. A positive probe cache
|
|
// is a latency optimization for List; it is never a source of failure or repair
|
|
// evidence.
|
|
func (c *AdapterProbeCache) readyWithin(key string, binaryPath string, now time.Time, ttl time.Duration) bool {
|
|
if c == nil {
|
|
return false
|
|
}
|
|
fingerprint, ok := readExecutableFingerprint(binaryPath)
|
|
if !ok {
|
|
return false
|
|
}
|
|
c.mu.RLock()
|
|
entry, found := c.entries[key]
|
|
c.mu.RUnlock()
|
|
if !found || !sameExecutableFingerprint(entry.fingerprint, fingerprint) {
|
|
return false
|
|
}
|
|
return ttl <= 0 || !now.After(entry.checkedAt.Add(ttl))
|
|
}
|
|
|
|
func (c *AdapterProbeCache) age(key string, binaryPath string, now time.Time) (time.Duration, bool) {
|
|
if c == nil {
|
|
return 0, false
|
|
}
|
|
fingerprint, ok := readExecutableFingerprint(binaryPath)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
c.mu.RLock()
|
|
entry, found := c.entries[key]
|
|
c.mu.RUnlock()
|
|
if !found || !sameExecutableFingerprint(entry.fingerprint, fingerprint) {
|
|
return 0, false
|
|
}
|
|
return now.Sub(entry.checkedAt), true
|
|
}
|
|
|
|
func (c *AdapterProbeCache) markReady(key string, binaryPath string) {
|
|
c.markReadyAt(key, binaryPath, time.Now())
|
|
}
|
|
|
|
func (c *AdapterProbeCache) markReadyAt(key string, binaryPath string, checkedAt time.Time) {
|
|
if c == nil {
|
|
return
|
|
}
|
|
fingerprint, ok := readExecutableFingerprint(binaryPath)
|
|
if !ok {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
c.entries[key] = adapterProbeCacheEntry{fingerprint: fingerprint, checkedAt: checkedAt}
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// DetectionCommandLimiter bounds actual auth/version/adapter subprocesses
|
|
// across concurrent List requests. Provider-level concurrency alone does not
|
|
// cover the multiple commands started inside each provider.
|
|
type DetectionCommandLimiter struct {
|
|
slots chan struct{}
|
|
}
|
|
|
|
func NewDetectionCommandLimiter(limit int) *DetectionCommandLimiter {
|
|
if limit < 1 {
|
|
limit = 1
|
|
}
|
|
return &DetectionCommandLimiter{slots: make(chan struct{}, limit)}
|
|
}
|
|
|
|
func (l *DetectionCommandLimiter) acquire(ctx context.Context) (func(), bool) {
|
|
if l == nil {
|
|
return func() {}, true
|
|
}
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
select {
|
|
case l.slots <- struct{}{}:
|
|
return func() { <-l.slots }, true
|
|
case <-ctx.Done():
|
|
return nil, false
|
|
}
|
|
}
|