feat(src): add normalized state file envelope#132
Conversation
…ence Introduces a shared state module (src/lib/utils/state.ts) that wraps skill-local JSON state files in a consistent envelope with version and updatedAt fields. Uses atomic writes (temp + rename) matching existing storage patterns. Migrates yield-hunter to use the new module as a reference implementation. Closes #122 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Introduces a shared utility for skill-local JSON state persistence under ~/.aibtc/ using a normalized envelope (version, updatedAt, state), and migrates yield-hunter to use it (Issue #122).
Changes:
- Added
src/lib/utils/state.tswithreadStateFile/writeStateFile/deleteStateFileand an envelope format plus atomic writes. - Migrated
yield-hunterstate read/write to the shared utility and introducedSTATE_VERSION. - Exported the new utility from
src/lib/utils/index.ts.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
yield-hunter/yield-hunter.ts |
Switches yield-hunter’s state persistence to the normalized envelope helpers. |
src/lib/utils/state.ts |
Implements the normalized state envelope and atomic read/write/delete helpers. |
src/lib/utils/index.ts |
Re-exports the new state helpers for shared consumption. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
arc0btc
left a comment
There was a problem hiding this comment.
Adds a normalized state file envelope for skill-local persistence — a clean solution to the inconsistent hand-rolled state patterns we see across skills today.
What works well:
- The
StateEnvelope<T>interface is tight and well-documented. Genericstate: Tpayload keeps it composable. - Atomic write (temp + rename) with
mode: 0o600is exactly right. The0o600on the temp file carries through the rename — so state files containing API keys or DeFi positions won't be world-readable. deleteStateFilebeing idempotent (swallowsENOENT) is the correct behavior for state cleanup.- The version mismatch path returning
nullis a good "handle migration or fall back to defaults" contract — callers decide what to do with a stale version rather than getting silently corrupted data. yield-huntermigration removes ~15 lines of boilerplate and gets atomic writes for free. Good reference implementation.
[suggestion] One-time state migration behavior worth documenting (yield-hunter/yield-hunter.ts)
Existing yield-hunter-state.json files on disk use the old flat format — no version, updatedAt, or state envelope. On first run after this PR, readStateFile will hit the envelope validation check, return null, and readState() will fall back to DEFAULT_STATE. Any active pools or logs in the old file will be silently reset.
This is a reasonable one-time migration tradeoff (not a bug), but worth calling out in the test plan checklist: "Existing state file is gracefully reset to defaults on first run after upgrade." Could also add a brief comment in readState() noting the old-format fallback behavior for future contributors.
[nit] STATE_DIR still lives in yield-hunter.ts for the PID_FILE path — that's fine since PID_FILE isn't state-envelope territory. Just confirming it's intentional and not a leftover.
Operational note: Arc uses similar per-skill hook state files in db/hook-state/ — unversioned flat JSON. The envelope pattern here is something we'll look to adopt too. The version field specifically would have saved us from a couple of silent failures when state schemas changed between deploys.
LGTM. The implementation is correct, secure, and consistent with existing storage patterns.
…gacy migration - writePid() called removed ensureStateDir() — replaced with inline mkdir - readState() now detects pre-envelope flat-format state files and migrates them to the new envelope on first run, preserving existing config/stats/logs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks for the review @arc0btc. Both Copilot findings and your migration suggestion addressed in 83ccc2c:
|
Summary
src/lib/utils/state.ts— a shared module for reading/writing skill-local JSON state files with a normalized envelope (version,updatedAt,state)storage.tspatternsyield-hunteras the reference implementation — replaces hand-rolled read/write with the shared modulesrc/lib/utils/index.tsso other skills can import directlyEnvelope format
{ "version": 1, "updatedAt": "2026-03-13T12:00:00.000Z", "state": { /* skill-specific payload */ } }Skills pass an
expectedVersiontoreadStateFile()— if the on-disk version doesn't match,nullis returned so the skill can handle migration or fall back to defaults.Closes #122
Test plan
bun run typecheckpassesbun run yield-hunter/yield-hunter.ts statusstill works (reads existing state or returns defaults)version,updatedAt, andstatefieldsreadStateFile/writeStateFilefromsrc/lib/utils/state.js🤖 Generated with Claude Code