fix: invalidate ContextFileInterceptor cache on agents.files.set (#29)

* fix: invalidate ContextFileInterceptor cache on agents.files.set

When agents.files.set (or agents.update) wrote context files to
agent_context_files in Postgres, only the agent router cache was
invalidated via agents.InvalidateAgent(). The ContextFileInterceptor
maintains its own separate in-memory cache with a 5-minute TTL which
was never cleared, causing the agent to serve stale file content
(e.g. wizard-written SOUL.md/IDENTITY.md) for up to 5 minutes.

Fix: inject *tools.ContextFileInterceptor into AgentsMethods and call
intc.InvalidateAgent(ag.ID) immediately after each successful DB write
in handleFilesSet and handleUpdate.

Wire path: wireManagedExtras now returns the interceptor it creates;
gateway.go captures it and passes it through registerAllMethods →
NewAgentsMethods. The interceptor is nil in standalone mode so all
nil guards are in place.

* test: add unit tests for ContextFileInterceptor cache invalidation

Covers:
- Cache hit: second read does not call store again
- InvalidateAgent clears agent cache → fresh content served from store
- InvalidateAgent clears user cache for that agent
- InvalidateAgent does not affect other agents' cache entries
- TTL expiry causes re-fetch from store
This commit is contained in:
therichardngai-code authored and GitHub committed 2026-03-01 21:44:29 +07:00
1 parent 82ce20a029
commit 8d513763f2
6 files changed
+288 -14

No files matched your search

+7 -2
View File
@@ -579,6 +579,11 @@ func runGateway() {
server.SetPolicyEngine(permPE)
server.SetPairingService(pairingStore)
// contextFileInterceptor is created inside wireManagedExtras (managed mode only).
// Declared here so it can be passed to registerAllMethods → AgentsMethods
// for immediate cache invalidation on agents.files.set.
var contextFileInterceptor *tools.ContextFileInterceptor
// Managed mode: set agent store for tools_invoke context injection + wire extras
if managedStores != nil && managedStores.Agents != nil {
server.SetAgentStore(managedStores.Agents)
@@ -593,7 +598,7 @@ func runGateway() {
}
}
wireManagedExtras(managedStores, agentRouter, providerRegistry, msgBus, sessStore, toolsReg, toolPE, skillsLoader, hasMemory, traceCollector, workspace, cfg.Gateway.InjectionAction, cfg, sandboxMgr, dynamicLoader)
contextFileInterceptor = wireManagedExtras(managedStores, agentRouter, providerRegistry, msgBus, sessStore, toolsReg, toolPE, skillsLoader, hasMemory, traceCollector, workspace, cfg.Gateway.InjectionAction, cfg, sandboxMgr, dynamicLoader)
agentsH, skillsH, tracesH, mcpH, customToolsH, channelInstancesH, providersH, delegationsH, builtinToolsH := wireManagedHTTP(managedStores, cfg.Gateway.Token, msgBus, toolsReg, providerRegistry, permPE.IsOwner)
if agentsH != nil {
server.SetAgentsHandler(agentsH)
@@ -654,7 +659,7 @@ func runGateway() {
teamStoreForRPC = managedStores.Teams
}
pairingMethods := registerAllMethods(server, agentRouter, sessStore, cronStore, pairingStore, cfg, cfgPath, workspace, dataDir, msgBus, execApprovalMgr, agentStoreForRPC, isManaged, skillStore, configSecretsStore, teamStoreForRPC)
pairingMethods := registerAllMethods(server, agentRouter, sessStore, cronStore, pairingStore, cfg, cfgPath, workspace, dataDir, msgBus, execApprovalMgr, agentStoreForRPC, isManaged, skillStore, configSecretsStore, teamStoreForRPC, contextFileInterceptor)
// Channel manager
channelMgr := channels.NewManager(msgBus)
+4 -1
View File
@@ -25,6 +25,8 @@ import (
// agent resolver (lazy-creates Loops from DB), virtual FS interceptors, memory tools,
// and cache invalidation event subscribers.
// PG store creation and tracing are handled in gateway.go before this is called.
// Returns the ContextFileInterceptor so callers can pass it to AgentsMethods
// for immediate cache invalidation on agents.files.set.
func wireManagedExtras(
stores *store.Stores,
agentRouter *agent.Router,
@@ -41,7 +43,7 @@ func wireManagedExtras(
appCfg *config.Config,
sandboxMgr sandbox.Manager,
dynamicLoader *tools.DynamicToolLoader,
) {
) *tools.ContextFileInterceptor {
// 1. Context file interceptor (created before resolver so callbacks can reference it)
var contextFileInterceptor *tools.ContextFileInterceptor
if stores.Agents != nil {
@@ -354,6 +356,7 @@ func wireManagedExtras(
}
slog.Info("managed mode: resolver + interceptors + cache subscribers wired")
return contextFileInterceptor
}
// wireManagedHTTP creates managed-mode HTTP handlers (agents + skills + traces + MCP + custom tools + channel instances + providers + delegations + builtin tools).
+2 -2
View File
@@ -12,12 +12,12 @@ import (
"github.com/nextlevelbuilder/goclaw/internal/tools"
)
func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore store.SessionStore, cronStore store.CronStore, pairingStore store.PairingStore, cfg *config.Config, cfgPath, workspace, dataDir string, msgBus *bus.MessageBus, execApprovalMgr *tools.ExecApprovalManager, agentStore store.AgentStore, isManaged bool, skillStore store.SkillStore, configSecretsStore store.ConfigSecretsStore, teamStore store.TeamStore) *methods.PairingMethods {
func registerAllMethods(server *gateway.Server, agents *agent.Router, sessStore store.SessionStore, cronStore store.CronStore, pairingStore store.PairingStore, cfg *config.Config, cfgPath, workspace, dataDir string, msgBus *bus.MessageBus, execApprovalMgr *tools.ExecApprovalManager, agentStore store.AgentStore, isManaged bool, skillStore store.SkillStore, configSecretsStore store.ConfigSecretsStore, teamStore store.TeamStore, contextFileInterceptor *tools.ContextFileInterceptor) *methods.PairingMethods {
router := server.Router()
// Phase 1: Core methods
methods.NewChatMethods(agents, sessStore, isManaged, server.RateLimiter()).Register(router)
methods.NewAgentsMethods(agents, cfg, cfgPath, workspace, agentStore, isManaged).Register(router)
methods.NewAgentsMethods(agents, cfg, cfgPath, workspace, agentStore, isManaged, contextFileInterceptor).Register(router)
methods.NewSessionsMethods(sessStore).Register(router)
methods.NewConfigMethods(cfg, cfgPath, isManaged, configSecretsStore).Register(router)