History, snapshots, retention

v0.3.48: snapshotting and History Restore do not require the OSConfig CLI. They read and write the local manifest store only. History Restore re-registers the selected snapshot into the local baseline store and creates an auto-snapshot of the previous source first. It does not call oscfg apply; device rollback is handled by the separate Revert action, which is CLI-gated.

Every save in ConfigForge writes a snapshot, a content-addressable copy of the source YAML plus an optional JSON sidecar. The snapshots collectively form a per-manifest history that underpins diff, restore, audit-pack export, and every other "what-was-it?" surface.

Where snapshots live

~/.configforge/history/<namespace>/
├── 2026-03-01T14-22-00.000Z.a3f8b1c4.osc.yaml      ← payload
├── 2026-03-01T14-22-00.000Z.a3f8b1c4.osc.yaml.meta ← sidecar (JSON)
├── 2026-03-01T14-27-12.000Z.7d3e1942.osc.yaml
└── 2026-03-01T14-27-12.000Z.7d3e1942.osc.yaml.meta

The filename is <ISO-8601-timestamp-with-dashes>.<8-hex-random-suffix>.osc.yaml and the sidecar appends .meta to the full filename.

Override the home directory with the CONFIGFORGE_HOME env var (used by tests).

Sidecar shape

{
  "message": "AuditAccountLockout modified",
  "author": "Alice Hardener",
  "authorEmail": "alice@example.com",
  "rationale": "OPM-12345 audit"
}
FieldSinceNotes
messagev0.3.47 real summariesOptional line shown in the History panel. Editor saves now use a real change summary when available (for example, AuditAccountLockout modified) instead of always Manifest registered.
author, authorEmailRationale captureResolved via resolveAuthor().
rationaleRationale captureThe "Why?" modal text.

The sidecar is written only when at least one field is non-empty. Old sidecars that predate these fields just carry message. The loader is defensive (any field with the wrong type is treated as absent). The History panel displays message below the timestamp, so v0.3.47+ snapshots show the diff summary rather than the generic registration label when the editor provided one.

Dedupe

Identical content saved twice in a row results in one history entry. The second save returns the existing entry rather than writing a duplicate. The check is content-hash-based (sha256 of the source YAML) and only compares against the IMMEDIATE PREDECESSOR so the operation stays O(1) regardless of history size.

Note: Dedupe deliberately preserves the first message / author / rationale. To truly rewrite metadata, delete the snapshot file and re-save.

Retention

The retention sweep runs after every successful save:

CONFIGFORGE_HISTORY_MAX_COUNT (default 50)
  -1 or 0   → disable pruning
  N > 0     → keep the N newest entries; oldest pruned

The sweep is best-effort. Failure never fails the save.

Settings store (v0.3.1+)

As of v0.3.1, you can configure history retention from the Settings page instead of relying on the environment variable. The allowed range is 5 to 1,000 snapshots. The Settings value takes precedence over CONFIGFORGE_HISTORY_MAX_COUNT when set.

Pre-deploy snapshot rotation

Each deploy now saves a timestamped backup of the manifest source alongside the latest pointer. The backup filename includes the ISO-8601 timestamp so you can correlate it with deploy logs. This means you always have a recoverable copy even if the deploy overwrites the working file.

Deploy-recovery banner

If a deploy is interrupted (process crash, forced close, power loss), the dashboard shows a recovery banner on the next launch. The banner links to the most recent pre-deploy snapshot so you can review and restore it. The banner dismisses automatically once you acknowledge or restore.

Restore

Restore replays a prior snapshot:

  1. Pre-restore snapshot - the current YAML is snapshotted before the restore runs. You can always undo a restore by restoring the pre-restore entry.
  2. The chosen snapshot is re-registered as the baseline source in the local manifest store.
  3. No device apply is performed. To roll back a deployed device, use the separate Revert action, which is CLI-gated.

The pre-restore snapshot's message is Auto-snapshot before restore of <id> so it's easy to spot in the history list.

Snapshot ID format

2026-03-01T14-22-00.000Z.a3f8b1c4
└─────────────┬─────────────┘ └────┬───┘
              │                    └─ 8 hex chars from randomBytes(4)
              └─ ISO-8601 UTC timestamp with `:` replaced by `-`

The format sorts lexicographically. ls returns chronological order. The 8-hex suffix is a random collision suffix (NOT a content hash); it disambiguates multiple writes within the same millisecond without paying a sha256 on the way in. Content-hash dedupe is a separate, predecessor-only check (see Dedupe above).

Performance & limits

ConstraintValue
Max retained entries (default)50
Sweep costO(N) per save where N = retained entries
Path traversalRefused at the security boundary
Concurrent writesPer-namespace mutex

IPC surface

The renderer talks to the history store through the typed preload bridge:

  • cfs.history.list({ name }) - list snapshot summaries (no payloads). Pass { name, id } to fetch one snapshot's payload + sidecar.
  • cfs.history.save({ name, content, message? }) - manual snapshot.
  • cfs.history.delete({ name, id }) - delete a snapshot.
  • Device Revert is separate from History Restore. It uses cfs.revert.apply / cfs:revert:apply to re-apply a pre-deploy snapshot or remove the namespace.

See API Reference → History / import / export.

See also