diff options
| author | t <t@tjp.lol> | 2026-09-25 14:58:55 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-09-25 15:11:20 -0600 |
| commit | 8da8849e71a8dac7ed81d45d7674e6a3f38cbb7c (patch) | |
| tree | 7c65f92bcf55eafca5b5c7ced18b66e1f5813518 /_selfcheck.lua | |
Add panto-warming session cache warming extension
Send a minimal transient prompt on idle intervals to keep the
provider-side prompt cache warm, then discard prompt and response so
the context window and transcript stay untouched.
Disabled by default; enable via warming table with layered interval,
duration, and prompt settings.
Diffstat (limited to '_selfcheck.lua')
| -rw-r--r-- | _selfcheck.lua | 223 |
1 files changed, 223 insertions, 0 deletions
diff --git a/_selfcheck.lua b/_selfcheck.lua new file mode 100644 index 0000000..fa6a0e2 --- /dev/null +++ b/_selfcheck.lua @@ -0,0 +1,223 @@ +-- Self-check for panto-warming. Run with: +-- +-- panto lua _selfcheck.lua +-- +-- Every module under test is pure Lua (no session, no panto module needed), +-- so plain lua5.4 runs it too. The fidelity probe at the bottom uses the +-- host's real JSON codec when the interpreter provides one. + +local here = (arg and arg[0] or "_selfcheck.lua"):match("^(.*)/") or "." +package.path = here .. "/?.lua;" .. here .. "/?/init.lua;" .. package.path + +local config = require("warming.config") +local policy = require("warming.policy") + +local SEC, MIN, HOUR = 1000, 60000, 3600000 + +-- --------------------------------------------------------------------------- +-- Duration strings +-- --------------------------------------------------------------------------- + +assert(config.parse_duration("4 minutes") == 4 * MIN, "4 minutes") +assert(config.parse_duration("30 seconds") == 30 * SEC, "30 seconds") +assert(config.parse_duration("1 hour") == HOUR, "1 hour") +assert(config.parse_duration("2h") == 2 * HOUR, "2h") +assert(config.parse_duration(" 10 mins ") == 10 * MIN, "whitespace and synonyms") + +local function duration_fails(text, why) + local ms, err = config.parse_duration(text) + assert(ms == nil, "parse_duration accepted " .. why) + assert(type(err) == "string", "parse_duration explains " .. why) +end +duration_fails("soon", "a non-duration") +duration_fails("4", "a bare number") +duration_fails("0 minutes", "a zero duration") +duration_fails("-1 minutes", "a negative duration") +duration_fails("1 fortnight", "an unknown unit") +duration_fails(4, "a non-string") + +-- --------------------------------------------------------------------------- +-- Layer merging +-- --------------------------------------------------------------------------- + +local s = config.defaults() +assert(s.enabled == false, "warming is off unless configured") +assert(s.interval_ms == 4 * MIN and s.duration_ms == 30 * MIN, "opencode defaults") +assert(s.prompt:find("OK"), "default prompt asks for OK") + +assert(config.apply_layer(s, true, "x") == true, "scalar true applies") +assert(s.enabled == true, "scalar true enables") +assert(config.apply_layer(s, { interval = "2 minutes" }, "x") == true, "table applies") +assert(s.interval_ms == 2 * MIN, "interval overridden") +assert(s.duration_ms == 30 * MIN, "omitted keys keep earlier values") +assert(config.apply_layer(s, false, "x") == true, "scalar false applies") +assert(s.enabled == false, "scalar false disables") +assert(config.apply_layer(s, { enabled = false, duration = "10 minutes" }, "x") == true, "enabled key applies") +assert(s.enabled == false, "warming.enabled = false disables") +assert(s.duration_ms == 10 * MIN, "duration still merged while disabled") +assert(config.apply_layer(s, { prompt = "stay put" }, "x") == true, "prompt applies") +assert(s.prompt == "stay put" and s.enabled == true, "table presence enables") + +local function layer_fails(value, why) + local ok, err = config.apply_layer(config.defaults(), value, "/x/config.toml") + assert(ok == nil, "apply_layer accepted " .. why) + assert(type(err) == "string" and err:find("/x/config.toml"), "error names the file for " .. why) +end +layer_fails("always", "a string scalar") +layer_fails({ bogus = 1 }, "an unknown key") +layer_fails({ prompt = "" }, "an empty prompt") +layer_fails({ interval = "1 fortnight" }, "a bad duration") +layer_fails({ enabled = "yes" }, "a non-boolean enabled") + +-- Layer walk over fake config layers (base < user < project < local). +local docs = { + ["/base/config.toml"] = { warming = true }, + ["/user/config.toml"] = { warming = { interval = "2 minutes" } }, + ["/project/config.toml"] = { warming = { duration = "10 minutes" } }, + ["/local/config.toml"] = { warming = false }, +} +local fake_io = { + read_file = function(path) + return docs[path] and path or nil -- the "file contents" are its path + end, + parse = function(text) + return docs[text] + end, +} +local fake_ext = { + dirs = { layers = { + { name = "base", dir = "/base" }, + { name = "user", dir = "/user" }, + { name = "project", dir = "/project" }, + { name = "local", dir = "/local" }, + { name = "missing", dir = "/nowhere" }, + } }, +} +local merged = assert(config.read(fake_ext, fake_io)) +assert(merged.interval_ms == 2 * MIN and merged.duration_ms == 10 * MIN, "layers merge per key") +assert(merged.enabled == false, "the highest layer's scalar wins") + +docs["/local/config.toml"] = { warming = { interval = "90 seconds" } } +merged = assert(config.read(fake_ext, fake_io)) +assert(merged.enabled == true and merged.interval_ms == 90 * SEC, "later layers enable and override") + +docs["/local/config.toml"] = { warming = { typo = true } } +local bad, err = config.read(fake_ext, fake_io) +assert(bad == nil and err:find("/local/config.toml"), "a bad layer fails with its path") + +docs["/local/config.toml"] = { warming = true } +fake_io.parse = function() error("boom") end +bad, err = config.read(fake_ext, fake_io) +assert(bad == nil and err:find("boom"), "a parse error surfaces its reason") + +-- --------------------------------------------------------------------------- +-- Timing policy: the opencode timelines +-- --------------------------------------------------------------------------- + +local function fire_times(p, from, to, step) + local fired = {} + local t = from + while t <= to do + if p:due(t) then + fired[#fired + 1] = t / MIN + p:attempted(t) + end + t = t + step + end + return fired +end + +-- 00:00 real request; warm every 4 minutes; stop at 30 minutes. +do + local p = policy.new(4 * MIN, 30 * MIN) + assert(p:expired(0), "no window before any activity") + p:activity(0) + assert(not p:due(0), "no warm request at the moment of activity") + local fired = fire_times(p, 15 * SEC, 40 * MIN, 15 * SEC) + assert(#fired == 7, "seven warm attempts in the default window, got " .. #fired) + for i, at in ipairs(fired) do + assert(at == i * 4, "warm attempt " .. i .. " at " .. at .. "m, want " .. (i * 4) .. "m") + end + assert(p:expired(30 * MIN), "window closes at 30m") + assert(not p:expired(29 * MIN), "window still open at 29m") +end + +-- 00:00 real request; 00:20 real request restarts both timers; warms at +-- 00:24; stops at 00:50. +do + local p = policy.new(4 * MIN, 30 * MIN) + p:turn_started(0) + assert(not p:due(4 * MIN), "a turn in flight blocks warming") + p:turn_ended(20 * MIN) + assert(not p:due(20 * MIN), "interval restarts after the turn") + assert(not p:due(23 * MIN), "not due at 23m") + assert(p:due(24 * MIN), "due at 24m") + p:attempted(24 * MIN) + assert(not p:due(27 * MIN + 59 * SEC), "next attempt is a full interval later") + assert(p:due(28 * MIN), "due at 28m") + assert(not p:expired(49 * MIN), "window runs to 50m") + assert(p:expired(50 * MIN), "window closes at 50m") +end + +-- An interval as long as the window: the window always expires first. +do + local p = policy.new(30 * MIN, 30 * MIN) + p:activity(0) + local fired = fire_times(p, 0, 60 * MIN, 15 * SEC) + assert(#fired == 0, "interval >= duration means no warm request ever") +end + +-- Entry eval contract: loading the extension chunk is side-effect-free and +-- returns the one activation entry. +local entry = assert(loadfile(here .. "/init.lua"))() +assert(type(entry) == "table" and entry.name == "warming" and type(entry.activate) == "function", + "init.lua returns the warming entry") + +-- --------------------------------------------------------------------------- +-- Tool-schema fidelity probe (report-only) +-- +-- Warming copies the session's tool declarations to the warm request +-- through the platform JSON codec: `agent:tools()` decodes the session's +-- schema bytes and `set_tools` re-encodes them. If encode(decode(x)) is not +-- byte-identical to x, the warm request's tool prefix diverges from the +-- session's own requests and cannot write a cache entry they will hit (see +-- the README). This probe measures how often that drift happens with the +-- codec in this interpreter. It reports, but does not fail: the drift is a +-- property of the codec, not of this rock. +-- --------------------------------------------------------------------------- + +local ok_panto, panto = pcall(require, "panto") +local json = ok_panto and type(panto) == "table" and panto.ext and panto.ext.json +if json and type(json.encode) == "function" then + local schemas = { + { type = "object", properties = {}, additionalProperties = false }, + { type = "object", properties = { query = { type = "string" } }, required = { "query" } }, + { type = "object", title = "run", properties = { + command = { type = "string" }, cwd = { type = "string" }, + timeout = { type = "integer", minimum = 1 }, + env = { type = "array", items = { type = "string" } }, + }, required = { "command" }, additionalProperties = false }, + { type = "object", properties = { + name = { type = "string" }, description = { type = "string" }, + schema = { type = "object", additionalProperties = true }, + labels = { type = "array", items = { type = "string" } }, + nested = { type = "object", properties = { + a = { type = "number" }, b = { type = "boolean" }, c = { type = "null" }, + } }, + } }, + } + local drift = 0 + for _, schema in ipairs(schemas) do + local encoded = json.encode(schema) + if json.encode(json.decode(encoded)) ~= encoded then + drift = drift + 1 + end + end + print(string.format( + "probe: schema re-encode drift %d/%d (%s)", + drift, #schemas, drift == 0 and "byte-stable" or "tool prefix fidelity degraded; see README")) +else + print("probe: skipped (no panto.ext.json in this interpreter)") +end + +print("panto-warming selfcheck: OK") |
