Skip to content

feat(src): add normalized state file envelope#132

Merged
whoabuddy merged 2 commits intomainfrom
feat/normalize-state-files
Mar 13, 2026
Merged

feat(src): add normalized state file envelope#132
whoabuddy merged 2 commits intomainfrom
feat/normalize-state-files

Conversation

@whoabuddy
Copy link
Contributor

Summary

  • Adds src/lib/utils/state.ts — a shared module for reading/writing skill-local JSON state files with a normalized envelope (version, updatedAt, state)
  • Uses atomic writes (temp file + rename) matching existing storage.ts patterns
  • Migrates yield-hunter as the reference implementation — replaces hand-rolled read/write with the shared module
  • Exports from src/lib/utils/index.ts so other skills can import directly

Envelope format

{
  "version": 1,
  "updatedAt": "2026-03-13T12:00:00.000Z",
  "state": { /* skill-specific payload */ }
}

Skills pass an expectedVersion to readStateFile() — if the on-disk version doesn't match, null is returned so the skill can handle migration or fall back to defaults.

Closes #122

Test plan

  • bun run typecheck passes
  • bun run yield-hunter/yield-hunter.ts status still works (reads existing state or returns defaults)
  • New state files written by yield-hunter include version, updatedAt, and state fields
  • Other skills can adopt by importing readStateFile/writeStateFile from src/lib/utils/state.js

🤖 Generated with Claude Code

…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>
Copilot AI review requested due to automatic review settings March 13, 2026 19:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts with readStateFile/writeStateFile/deleteStateFile and an envelope format plus atomic writes.
  • Migrated yield-hunter state read/write to the shared utility and introduced STATE_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.

Copy link
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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. Generic state: T payload keeps it composable.
  • Atomic write (temp + rename) with mode: 0o600 is exactly right. The 0o600 on the temp file carries through the rename — so state files containing API keys or DeFi positions won't be world-readable.
  • deleteStateFile being idempotent (swallows ENOENT) is the correct behavior for state cleanup.
  • The version mismatch path returning null is 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-hunter migration 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>
@whoabuddy
Copy link
Contributor Author

Thanks for the review @arc0btc. Both Copilot findings and your migration suggestion addressed in 83ccc2c:

  • writePid() now has its own fs.mkdir(STATE_DIR, { recursive: true }) — the ensureStateDir() removal was a real bug
  • readState() detects legacy flat-format files (checks for config + stats keys without a state wrapper) and migrates them to the envelope on first run, preserving existing config/stats/logs
  • STATE_DIR retained intentionally for PID file path — not envelope territory, as you noted

@whoabuddy whoabuddy merged commit 56c1346 into main Mar 13, 2026
1 check passed
@whoabuddy whoabuddy deleted the feat/normalize-state-files branch March 13, 2026 20:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

normalize local json files for state

3 participants