Summary
getValueOf memoizes results in a package-level cache that lives for the entire process and is never invalidated. In the long-lived MCP server, once a file:key pair is read it is frozen for the life of the process — even if hulak itself rewrites that file. This breaks the common auth pattern where one request saves a token response and a later request reads it with getValueOf.
Reproduction
A request reads a token from a response file another request writes:
# getUserPlan.hk.yaml
headers:
Authorization: Bearer {{getValueOf "access_token" "auth/getAuth.hk_response.json"}}
In a single MCP-server session:
call_request getUserPlan → the token file holds an old/expired token → request fails 401. (This first read caches the old token.)
call_request getAuth --save → HTTP 200, writes a fresh token to auth/getAuth.hk_response.json. Confirmed on disk (cat shows the fresh token).
call_request getUserPlan again → still 401. Debug output shows the Authorization header carrying the old token, not the fresh one on disk.
- Repeating never helps. Hardcoding the token makes the call succeed, confirming everything else is fine.
Root cause
A process-lifetime cache in pkg/actions/actions.go:
pkg/actions/actions.go:24-30 — package-global valuesCache map[string]valueCache, lives for the whole process.
GetValueOf (pkg/actions/actions.go:33-64) keys the cache as "<fileName>:<key>". On a cache hit it returns the stored value and never reads the file; only on a miss does it call processValueOf → readJSONFile (actions.go:211-244, a real os.ReadFile), then stores the result permanently. There is no invalidation anywhere — no TTL, no mtime check, no delete.
- The MCP server is a single long-lived process (
pkg/mcp/server.go:193), so valuesCache persists across every call_request.
- The save path (
pkg/apiCalls/writeResponse.go:147, os.WriteFile) writes the file but never touches valuesCache, so the cache goes stale relative to disk.
getValueOf is wired as a template func evaluated on every request (pkg/envparser/replaceVars.go:94), so step 1 seeds the cache and steps 3+ hit it.
Ruled out (not the cause)
- Path/instance mismatch — read (
resolveJSONFilePath → filepath.Abs, actions.go:156-171) and write (writeResponse.go:132-134) both resolve against the same project dir to the same file; the on-disk cat confirms the write landed where the read resolves.
- Write/flush race — the save is a synchronous, fully-flushed
os.WriteFile that completes before the next call.
Proposed fix
Drop the process-lifetime memoization for getValueOf (or gate readJSONFile behind an mtime check). A cache keyed only on file:key with no mtime/generation component is fundamentally unsafe for files hulak itself rewrites mid-process — which is exactly the getAuth → getValueOf pattern. The memoization buys little for response files that change between calls and is the direct cause of the staleness.
(A more minimal alternative — evict matching cache entries on every response-file write via an exported actions.InvalidateFile(path) — would also work, but dropping/mtime-gating the cache is the cleaner, correct-by-construction fix.)
Summary
getValueOfmemoizes results in a package-level cache that lives for the entire process and is never invalidated. In the long-lived MCP server, once afile:keypair is read it is frozen for the life of the process — even if hulak itself rewrites that file. This breaks the common auth pattern where one request saves a token response and a later request reads it withgetValueOf.Reproduction
A request reads a token from a response file another request writes:
In a single MCP-server session:
call_request getUserPlan→ the token file holds an old/expired token → request fails 401. (This first read caches the old token.)call_request getAuth --save→ HTTP 200, writes a fresh token toauth/getAuth.hk_response.json. Confirmed on disk (catshows the fresh token).call_request getUserPlanagain → still 401. Debug output shows theAuthorizationheader carrying the old token, not the fresh one on disk.Root cause
A process-lifetime cache in
pkg/actions/actions.go:pkg/actions/actions.go:24-30— package-globalvaluesCache map[string]valueCache, lives for the whole process.GetValueOf(pkg/actions/actions.go:33-64) keys the cache as"<fileName>:<key>". On a cache hit it returns the stored value and never reads the file; only on a miss does it callprocessValueOf→readJSONFile(actions.go:211-244, a realos.ReadFile), then stores the result permanently. There is no invalidation anywhere — no TTL, no mtime check, no delete.pkg/mcp/server.go:193), sovaluesCachepersists across everycall_request.pkg/apiCalls/writeResponse.go:147,os.WriteFile) writes the file but never touchesvaluesCache, so the cache goes stale relative to disk.getValueOfis wired as a template func evaluated on every request (pkg/envparser/replaceVars.go:94), so step 1 seeds the cache and steps 3+ hit it.Ruled out (not the cause)
resolveJSONFilePath→filepath.Abs,actions.go:156-171) and write (writeResponse.go:132-134) both resolve against the same project dir to the same file; the on-diskcatconfirms the write landed where the read resolves.os.WriteFilethat completes before the next call.Proposed fix
Drop the process-lifetime memoization for
getValueOf(or gatereadJSONFilebehind an mtime check). A cache keyed only onfile:keywith no mtime/generation component is fundamentally unsafe for files hulak itself rewrites mid-process — which is exactly thegetAuth→getValueOfpattern. The memoization buys little for response files that change between calls and is the direct cause of the staleness.(A more minimal alternative — evict matching cache entries on every response-file write via an exported
actions.InvalidateFile(path)— would also work, but dropping/mtime-gating the cache is the cleaner, correct-by-construction fix.)