Skip to content

fix: add schema versioning to history.json (Fixes #2252)#2256

Merged
la14-1 merged 2 commits intomainfrom
fix/issue-2252
Mar 6, 2026
Merged

fix: add schema versioning to history.json (Fixes #2252)#2256
la14-1 merged 2 commits intomainfrom
fix/issue-2252

Conversation

@la14-1
Copy link
Copy Markdown
Member

@la14-1 la14-1 commented Mar 6, 2026

Summary

  • Adds a version field to history.json using a new v1 envelope: { "version": 1, "records": [...] }
  • loadHistory() transparently reads both v0 (bare array) and v1 formats — v0 files are silently migrated to v1 on the next write
  • All write operations now go through a shared writeHistory() helper that stamps HISTORY_SCHEMA_VERSION consistently
  • Valibot schemas (VMConnectionSchema, SpawnRecordSchema, HistoryFileV1Schema) validate the file structure without as casts
  • Updated all affected tests to match the new on-disk format (data.records instead of data)

Why

Without schema versioning, any future rename/add/remove of a field in SpawnRecord or VMConnection causes silent data loss, silent corruption, or confusing empty-history bugs for users who upgrade. The versioned envelope creates the migration escape hatch described in the issue.

Migration

  • v0 files (old bare arrays) continue to load correctly — no user action needed
  • On the first write after upgrade, the file is transparently rewritten in v1 format
  • Future schema changes can add v1→v2 migration logic in loadHistory()

Fixes #2252

-- refactor/issue-fixer


Open with Devin

Copy link
Copy Markdown
Member

@louisgv louisgv left a comment

Choose a reason for hiding this comment

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

Security Review

Verdict: APPROVED
Commit: 588969a

Findings

No security issues found.

Changes

  • Adds schema versioning to history.json (v1 format: { version: 1, records: [...] })
  • Introduces valibot schemas for validation (HistoryFileV1Schema, SpawnRecordSchema, VMConnectionSchema)
  • Centralizes write logic in writeHistory() function with consistent 0o600 permissions
  • Maintains backward compatibility with v0 format (bare array)
  • All write operations now enforced through single writeHistory() function

Security Properties

  • ✅ Schema validation via valibot prevents malformed data
  • ✅ Path safety via existing getHistoryPath() validation
  • ✅ No command injection vectors (pure JSON I/O)
  • ✅ Proper file permissions (0o600) on all writes
  • ✅ Unknown versions safely rejected (return empty array)
  • ✅ Backward compatibility preserved for v0 bare arrays

Tests

  • bash -n: N/A (no shell scripts modified)
  • bun test: PASS (103 tests, 310 assertions)
  • curl|bash: N/A
  • macOS compat: N/A

-- security/pr-reviewer

@louisgv louisgv added the security-approved Security review approved label Mar 6, 2026
Fixes #2252

history.json now uses a versioned envelope:
  { "version": 1, "records": [...] }

This creates a migration escape hatch for future SpawnRecord shape changes.
loadHistory() transparently reads both v0 (bare array) and v1 formats,
automatically migrating v0 files on next write. All write operations now
use writeHistory() to stamp the current schema version consistently.

Validation uses valibot schemas (VMConnectionSchema, SpawnRecordSchema,
HistoryFileV1Schema) so the structure is verified and typed without `as`
casts. Updated all affected tests to check data.records instead of data.

Agent: issue-fixer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@la14-1 la14-1 merged commit 2fd3175 into main Mar 6, 2026
5 of 6 checks passed
@la14-1 la14-1 deleted the fix/issue-2252 branch March 6, 2026 23:17
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +47 to +61
const SpawnRecordSchema = v.object({
id: v.optional(v.string()),
agent: v.string(),
cloud: v.string(),
timestamp: v.string(),
name: v.optional(v.string()),
prompt: v.optional(v.string()),
connection: v.optional(VMConnectionSchema),
});

/** v1 history file format: { version: 1, records: SpawnRecord[] } */
const HistoryFileV1Schema = v.object({
version: v.literal(1),
records: v.array(SpawnRecordSchema),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 valibot v.object() silently strips unknown keys — potential forward-compatibility concern

In valibot v1.2.0, v.object() strips unknown keys from the parsed output (unlike v.looseObject() which preserves them). The SpawnRecordSchema and VMConnectionSchema at packages/cli/src/history.ts:35-61 use v.object(). Currently all fields of SpawnRecord and VMConnection are covered by the schemas, so no data loss occurs. However, if a future CLI version adds a new field to SpawnRecord (e.g., duration) and writes v1 format, then a user running this older version would silently strip that field on the next load-then-write cycle. Using v.looseObject() instead of v.object() for the record schemas would make this forward-compatible without data loss risk.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

security-approved Security review approved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: ~/.spawn/history.json has no schema version — upgrades that change SpawnRecord shape silently corrupt history

2 participants