Problem
core/hci.lua holds mutable runtime state — the persisted lifetime accumulator behind +runtime show / +uptime / +hubinfo and GET /v1/runtime:
hci_tbl = {
[ "hubruntime" ] = 0,
[ "hubruntime_last_check" ] = 0,
}
It is read and rewritten via util.loadtable / util.savetable by hub_runtime (60s onTimer), cmd_uptime and cmd_hubinfo.
But it lives under core/, which is a shipped directory:
CMakeLists.txt:117 — install(DIRECTORY core/ DESTINATION core), wholesale, no exclusion.
- Docker (
docs/DOCKER.md) mounts ./cfg/ and ./scripts/; core/ is baked into the image.
So the pristine hubruntime = 0 ships over the operator''s accumulated value:
- Source install:
cmake --install into an existing tree → counter reset to 0.
- Docker: every image update → fresh
core/hci.lua → counter reset to 0.
hub_runtime''s check_hci() only guards a missing or corrupt file (it recreates zeros); a zeroed file is indistinguishable from a fresh hub, so nothing detects the loss.
Net effect: the "total runtime" figure an operator has been accumulating for months silently returns to zero on upgrade — which defeats the entire purpose of the accumulator (session uptime is already available separately via signal.get("start")).
Why it happened
This is the one piece of plugin state living under core/ instead of scripts/data/<plugin>.tbl, contrary to the rule in CLAUDE.md §7 / docs/DEVELOPMENT.md §3. The rule exists precisely for this: scripts/ is a mounted volume in Docker and is not clobbered by an upgrade, core/ is neither.
Note core/hci.lua is not a module (it is deliberately absent from init.lua''s _core array — that part is correct and should stay); it is a data file that happens to sit in the wrong directory.
Proposed fix
- Move the store to
scripts/data/hub_runtime.tbl (owner: hub_runtime, which is the only writer on the timer path).
- Migrate once: on
onStart, if scripts/data/hub_runtime.tbl is absent but core/hci.lua exists with a non-zero hubruntime, adopt its value and write the new file. Without this, the fix itself would zero everyone''s counter — the exact bug it repairs.
- Update the three readers (
hub_runtime, cmd_uptime, cmd_hubinfo) and the /v1/runtime GET/PUT handlers.
- Delete
core/hci.lua once the migration path has shipped for a release (or leave it read-only as the migration source and stop writing it).
docs/HTTP_API.md footnote for /v1/runtime names core/hci.lua as the backing store — update.
Verification
- Regression that fails pre-fix: seed a non-zero accumulator, re-run
cmake --install over the tree, assert the value survives (pre-fix it is 0).
- Migration test: old
core/hci.lua with a value + no new file → value adopted; new file present → old file ignored.
Found by the dead-code audit (core/hci.lua was flagged as a possible dead/stub module — it is neither, but the check surfaced this). CLAUDE.md §3 previously mislabelled it as a "(stub)"; corrected separately in the doc-currency pass.
Problem
core/hci.luaholds mutable runtime state — the persisted lifetime accumulator behind+runtime show/+uptime/+hubinfoandGET /v1/runtime:It is read and rewritten via
util.loadtable/util.savetablebyhub_runtime(60sonTimer),cmd_uptimeandcmd_hubinfo.But it lives under
core/, which is a shipped directory:CMakeLists.txt:117—install(DIRECTORY core/ DESTINATION core), wholesale, no exclusion.docs/DOCKER.md) mounts./cfg/and./scripts/;core/is baked into the image.So the pristine
hubruntime = 0ships over the operator''s accumulated value:cmake --installinto an existing tree → counter reset to 0.core/hci.lua→ counter reset to 0.hub_runtime''scheck_hci()only guards a missing or corrupt file (it recreates zeros); a zeroed file is indistinguishable from a fresh hub, so nothing detects the loss.Net effect: the "total runtime" figure an operator has been accumulating for months silently returns to zero on upgrade — which defeats the entire purpose of the accumulator (session uptime is already available separately via
signal.get("start")).Why it happened
This is the one piece of plugin state living under
core/instead ofscripts/data/<plugin>.tbl, contrary to the rule inCLAUDE.md§7 /docs/DEVELOPMENT.md§3. The rule exists precisely for this:scripts/is a mounted volume in Docker and is not clobbered by an upgrade,core/is neither.Note
core/hci.luais not a module (it is deliberately absent frominit.lua''s_corearray — that part is correct and should stay); it is a data file that happens to sit in the wrong directory.Proposed fix
scripts/data/hub_runtime.tbl(owner:hub_runtime, which is the only writer on the timer path).onStart, ifscripts/data/hub_runtime.tblis absent butcore/hci.luaexists with a non-zerohubruntime, adopt its value and write the new file. Without this, the fix itself would zero everyone''s counter — the exact bug it repairs.hub_runtime,cmd_uptime,cmd_hubinfo) and the/v1/runtimeGET/PUT handlers.core/hci.luaonce the migration path has shipped for a release (or leave it read-only as the migration source and stop writing it).docs/HTTP_API.mdfootnote for/v1/runtimenamescore/hci.luaas the backing store — update.Verification
cmake --installover the tree, assert the value survives (pre-fix it is 0).core/hci.luawith a value + no new file → value adopted; new file present → old file ignored.Found by the dead-code audit (
core/hci.luawas flagged as a possible dead/stub module — it is neither, but the check surfaced this).CLAUDE.md§3 previously mislabelled it as a "(stub)"; corrected separately in the doc-currency pass.