Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

141 lines
3.6 KiB
Go

package mcp
import (
"sort"
"sync"
"time"
"github.com/zzet/gortex/internal/persistence"
)
// demoteAfterSessions is the hysteresis window: a learned tool promotion
// unused for this many session epochs (server startups) is demoted back to
// the deferred catalogue. Small enough that a stale promotion clears out
// within a few sessions, large enough that an occasional-use tool sticks.
const demoteAfterSessions = 3
// promotedToolsManager is the per-workspace learned tool surface: tools
// that started deferred behind tools_search but were promoted into the
// eager surface after use, persisted so the learning survives daemon
// restarts. It advances a session epoch on load, demotes promotions unused
// for demoteAfterSessions epochs, and records fresh promotions/uses. A nil
// sidecar (no cache dir) degrades to an in-memory-only, non-persistent
// surface.
type promotedToolsManager struct {
mu sync.Mutex
sidecar *persistence.SidecarStore
repoKey string
epoch int64
live map[string]persistence.PromotedToolEntry
}
func newPromotedToolsManager(cacheDir, repoPath string) *promotedToolsManager {
m := &promotedToolsManager{live: map[string]persistence.PromotedToolEntry{}}
if cacheDir == "" || repoPath == "" {
return m
}
sidecar, err := persistence.OpenSidecar(persistence.DefaultSidecarPath(cacheDir))
if err != nil || sidecar == nil {
return m
}
m.sidecar = sidecar
m.repoKey = persistence.RepoCacheKey(repoPath)
return m
}
// Load advances the session epoch, demotes promotions unused past the
// hysteresis window, and returns the surviving tool names to re-promote
// into the eager surface. Call once at server startup.
func (m *promotedToolsManager) Load() []string {
if m == nil {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
if m.sidecar == nil {
return nil
}
epoch, err := m.sidecar.BumpSessionEpoch(m.repoKey)
if err != nil {
return nil
}
m.epoch = epoch
rows, err := m.sidecar.LoadPromotedTools(m.repoKey)
if err != nil {
return nil
}
var survivors []string
for _, e := range rows {
if epoch-e.LastUsedEpoch >= demoteAfterSessions {
_ = m.sidecar.DeletePromotedTool(m.repoKey, e.Tool) // demote
continue
}
m.live[e.Tool] = e
survivors = append(survivors, e.Tool)
}
sort.Strings(survivors)
return survivors
}
// Record persists a tool promotion (or refreshes an existing one's
// last-used epoch, resetting its demotion clock). workspaceID is stored for
// diagnostics; the surface is keyed per repo.
func (m *promotedToolsManager) Record(tool, workspaceID string) {
if m == nil || tool == "" {
return
}
m.mu.Lock()
defer m.mu.Unlock()
if m.sidecar == nil {
return
}
e, ok := m.live[tool]
if !ok {
e = persistence.PromotedToolEntry{Tool: tool, PromotedEpoch: m.epoch}
}
if workspaceID != "" {
e.WorkspaceID = workspaceID
}
e.LastUsedEpoch = m.epoch
e.UseCount++
e.UpdatedAt = time.Now()
m.live[tool] = e
_ = m.sidecar.UpsertPromotedTool(m.repoKey, e)
}
// Has reports whether a tool is in the learned surface.
func (m *promotedToolsManager) Has(tool string) bool {
if m == nil {
return false
}
m.mu.Lock()
defer m.mu.Unlock()
_, ok := m.live[tool]
return ok
}
// Names returns the learned surface, sorted.
func (m *promotedToolsManager) Names() []string {
if m == nil {
return nil
}
m.mu.Lock()
defer m.mu.Unlock()
out := make([]string, 0, len(m.live))
for t := range m.live {
out = append(out, t)
}
sort.Strings(out)
return out
}
// Count returns the size of the learned surface.
func (m *promotedToolsManager) Count() int {
if m == nil {
return 0
}
m.mu.Lock()
defer m.mu.Unlock()
return len(m.live)
}