@wipcomputer/wip-ldm-os 0.4.87-alpha.1 → 0.4.87-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/ldm.js CHANGED
@@ -724,7 +724,7 @@ async function installCatalogComponent(c) {
724
724
  }
725
725
 
726
726
  // ── Bridge deploy (#245) ──
727
- // Deploy all scripts from scripts/ to ~/.ldm/bin/
727
+ // Deploy runtime-owned backup and shell files from scripts/ to ~/.ldm/bin/
728
728
  // Called from both cmdInit() and cmdInstallCatalog() so script fixes land on every update.
729
729
  function deployScripts() {
730
730
  const scriptsSrc = join(__dirname, '..', 'scripts');
@@ -732,15 +732,17 @@ function deployScripts() {
732
732
  mkdirSync(join(LDM_ROOT, 'bin'), { recursive: true });
733
733
  let count = 0;
734
734
  for (const file of readdirSync(scriptsSrc)) {
735
- if (!file.endsWith('.sh')) continue;
735
+ const isShell = file.endsWith('.sh');
736
+ const isBackupRuntime = file === 'ldm-backup-engine.py' || file === 'ldm-backup-policy.json';
737
+ if (!isShell && !isBackupRuntime) continue;
736
738
  const src = join(scriptsSrc, file);
737
739
  const dest = join(LDM_ROOT, 'bin', file);
738
740
  cpSync(src, dest);
739
- chmodSync(dest, 0o755);
741
+ chmodSync(dest, file.endsWith('.json') ? 0o644 : 0o755);
740
742
  count++;
741
743
  }
742
744
  if (count > 0) {
743
- console.log(` + ${count} script(s) deployed to ~/.ldm/bin/`);
745
+ console.log(` + ${count} runtime file(s) deployed to ~/.ldm/bin/`);
744
746
  }
745
747
  return count;
746
748
  }
@@ -1,108 +1,111 @@
1
1
  # Backup
2
2
 
3
- ## One Script, One Place
3
+ LDM OS uses one policy-driven backup engine. The scheduled job remains separately controlled by LaunchAgent `ai.openclaw.ldm-backup`.
4
4
 
5
- `~/.ldm/bin/ldm-backup.sh` runs daily at 3:00 AM via LaunchAgent `ai.openclaw.ldm-backup`. It backs up everything to `~/.ldm/backups/`, then tars it to iCloud for offsite.
5
+ ## Safe dry run
6
6
 
7
- ## What Gets Backed Up
7
+ Run this after the repaired engine is released and installed:
8
8
 
9
- | Source | Method | What's in it |
10
- |--------|--------|-------------|
11
- | `~/.ldm/memory/crystal.db` | sqlite3 .backup | Irreplaceable memory (all agents) |
12
- | `~/.ldm/agents/` | cp -a | Identity files, journals, daily logs |
13
- | `~/.ldm/state/` | cp -a | Config, version, registry |
14
- | `~/.ldm/config.json` | cp | Workspace pointer, org |
15
- | `~/.openclaw/memory/main.sqlite` | sqlite3 .backup | OC conversations |
16
- | `~/.openclaw/memory/context-embeddings.sqlite` | sqlite3 .backup | Embeddings |
17
- | `~/.openclaw/workspace/` | tar | Shared context, daily logs |
18
- | `~/.openclaw/agents/main/sessions/` | tar | OC session JSONL |
19
- | `~/.openclaw/openclaw.json` | cp | OC config |
20
- | `~/.claude/CLAUDE.md` | cp | CC instructions |
21
- | `~/.claude/settings.json` | cp | CC settings |
22
- | `~/.claude/projects/` | tar | CC auto-memory + transcripts |
23
- | Workspace directory | tar (excludes node_modules, .git/objects, old backups, _trash) | Entire workspace |
9
+ ```bash
10
+ ~/.ldm/bin/ldm-backup.sh --dry-run
11
+ ```
24
12
 
25
- **NOT backed up:** node_modules/, .git/objects/ (reconstructable), extensions (reinstallable), ~/.claude/cache.
13
+ The dry run reads the same canonical policy and estimator as a real run. It reports every source classification, exclusions and reasons, per-source estimates, total estimated new bytes, disk capacity, available bytes, required reserve, planned staging and destination paths, blocked requirements, and the preflight verdict.
26
14
 
27
- ## Backup Structure
15
+ Dry run is strictly read-only. It does not create a directory, acquire a persistent lock, write structured state, create a manifest or completion marker, copy data, rotate snapshots, access Vault or iCloud for writing, change the schedule, enable the LaunchAgent, or modify containment state. Sensitive sources use policy display labels. Their filenames and contents are not enumerated.
28
16
 
29
- ```
30
- ~/.ldm/backups/2026-03-24--09-50-22/
31
- ldm/
32
- memory/crystal.db
33
- agents/
34
- state/
35
- config.json
36
- openclaw/
37
- memory/main.sqlite
38
- memory/context-embeddings.sqlite
39
- workspace.tar
40
- sessions.tar
41
- openclaw.json
42
- claude/
43
- CLAUDE.md
44
- settings.json
45
- projects.tar
46
- <workspace>.tar
47
- ```
17
+ Dry run exits zero only when the same plan would pass real-run preflight. Required missing sources, blocked encryption gates, destination failure, and reserve failure return nonzero.
48
18
 
49
- ## iCloud Offsite
19
+ ## Canonical policy
50
20
 
51
- After local backup, the entire dated folder is compressed and copied to iCloud. The destination path is read from `~/.ldm/config.json` at `paths.icloudBackup`.
21
+ The installed `~/.ldm/bin/ldm-backup-policy.json` is the source of truth for:
52
22
 
53
- One file per backup. iCloud syncs it across devices. Rotation matches the local retention setting.
23
+ - Required, rebuildable, sensitive, blocked, manifest-only, and excluded sources.
24
+ - Per-source capture types and destinations.
25
+ - Exact file-selection and exclusion rules.
26
+ - Disk-floor percentages.
27
+ - The two legal evidence states: `complete` and `deferred-after-write-failure`.
54
28
 
55
- ## How to Run
29
+ Estimation, capture, manifest generation, and exclusion records all consume this policy. Sensitive sources that still require encryption are marked blocked. The engine fails honestly instead of silently omitting them.
56
30
 
57
- ```bash
58
- ~/.ldm/bin/ldm-backup.sh # run backup now
59
- ~/.ldm/bin/ldm-backup.sh --dry-run # preview what would be backed up
60
- ~/.ldm/bin/ldm-backup.sh --keep 14 # keep 14 days instead of 7
61
- ~/.ldm/bin/ldm-backup.sh --include-secrets # include ~/.ldm/secrets/
62
- ```
31
+ The policy explicitly represents every Slice 1 inventory row. The fixture suite asserts the exact source-ID set so deleting or silently omitting a source fails testing.
63
32
 
64
- You can also run via the CLI:
33
+ ## Runtime dependency
65
34
 
66
- ```bash
67
- ldm backup # run backup now
68
- ldm backup --dry-run # preview with sizes
69
- ldm backup --pin "before upgrade" # pin latest backup so rotation skips it
70
- ```
35
+ The engine requires Python 3 and uses only the Python standard library. No third-party Python packages are required. The shell entry point checks `command -v python3` first and reports `Backup failed [python3-missing]` when the runtime is unavailable. Parker's operational acceptance of this release prerequisite remains a separate release gate.
71
36
 
72
- ## How to Restore
37
+ Configuration is fail-closed. A missing `~/.ldm/config.json` uses documented defaults, but an unreadable, malformed, or structurally invalid config stops planning with `config-invalid`.
73
38
 
74
- ```bash
75
- ~/.ldm/bin/ldm-restore.sh # list available backups
76
- ~/.ldm/bin/ldm-restore.sh 2026-03-24--09-50-22 # restore everything
77
- ~/.ldm/bin/ldm-restore.sh --only ldm <backup> # restore only crystal.db + agents
78
- ~/.ldm/bin/ldm-restore.sh --only openclaw <backup> # restore only OC data
79
- ~/.ldm/bin/ldm-restore.sh --from-icloud <file> # restore from iCloud tar
80
- ~/.ldm/bin/ldm-restore.sh --dry-run <backup> # preview
39
+ ## Mutation consistency
40
+
41
+ The dry-run estimate is a plan-time observation. A real run freshly observes each source immediately before that source is captured. Changes between planning and capture are allowed, recorded in the manifest through plan-time and capture-time metadata, and captured from the newer observation.
42
+
43
+ Regular files, symlinks, directory membership, and tree metadata must remain stable during their individual source capture. A same-size content change is detected by checksum. Any change during capture is fatal and leaves the snapshot in progress without completion proof. SQLite sources use the SQLite backup API as a transactionally consistent snapshot and are labeled `sqlite-backup-api-transaction-snapshot`.
44
+
45
+ When a source observation changes before capture, the engine recalculates the reserve before copying. The recalculation includes bytes already staged, stat-only refreshed estimates for remaining sources, a full checksum observation of the source about to be copied, projected metadata, and the 10 percent required reserve. Growth that crosses the floor fails with `capture-reserve-gate` before the changed source is copied.
46
+
47
+ ## Manifest evidence
48
+
49
+ `manifest` is an evidence-producing action, not a descriptive policy label. Implemented path-inventory generators emit a machine-readable member inventory, logical byte count, and inventory checksum. Every required manifest action must produce evidence that passes completion validation. Entries without an implemented generator are marked blocked before any filesystem walk. Required system and repo-aware Git/worktree manifests whose generators are not implemented remain explicitly blocked.
50
+
51
+ ## Successful snapshot contract
52
+
53
+ A real internal-tier run can report success only after:
54
+
55
+ 1. The process lock is acquired.
56
+ 2. Required sources and policy gates pass.
57
+ 3. Free space passes the 10 percent post-write reserve.
58
+ 4. Capture completes under `.in-progress-<snapshot-id>`.
59
+ 5. SQLite backups pass `PRAGMA quick_check`.
60
+ 6. Required manifest actions produce validated evidence.
61
+ 7. Every captured file has a manifest checksum.
62
+ 8. The manifest validates.
63
+ 9. `completion.json` is written last with atomic rename.
64
+ 10. The staging directory is atomically promoted.
65
+
66
+ Any required-source or validation failure exits nonzero, writes a fixture-visible failure result when possible, leaves no completion marker, and never prints `Backup complete`.
67
+
68
+ ## Snapshot layout
69
+
70
+ ```text
71
+ ~/.ldm/backups/v1/internal/
72
+ .in-progress-2026-07-11--19-00-00Z/
73
+ 2026-07-11--19-00-00Z/
74
+ ldm/
75
+ openclaw/
76
+ claude/
77
+ codex/
78
+ manifest.json
79
+ completion.json
81
80
  ```
82
81
 
83
- After restore: `openclaw gateway restart` then `crystal status` to verify.
82
+ Only verified completed snapshots participate in retention. Pinned snapshots never consume an unpinned retention slot. In-progress and unverified directories are never rotated.
83
+
84
+ ## Structured state
85
+
86
+ The engine defines atomic records under `~/.ldm/state/backup/v1/`:
84
87
 
85
- ## Schedule
88
+ - `schedule.json`
89
+ - `attempt.json`
90
+ - `result.json`
91
+ - `success-local.json`
86
92
 
87
- | What | When | How |
88
- |------|------|-----|
89
- | Backup | 3:00 AM | LaunchAgent `ai.openclaw.ldm-backup` |
93
+ Vault and iCloud success records are reserved for their later slices. State is diagnostic. Snapshot proof requires the manifest and completion marker.
90
94
 
91
- One LaunchAgent. One script. No Full Disk Access currently (target: midnight via LDMDevTools.app once PID error is fixed). Verify is built into the script (exit code + log).
95
+ Snapshot identifiers use UTC and end in `Z`, which prevents local DST fall-back collisions. When no valid containment marker exists, `schedule.json` records `evidence_status: unknown`; it never invents a deferred write failure.
92
96
 
93
- ## Config
97
+ ## Fixture suite
94
98
 
95
- All backup settings live in `~/.ldm/config.json`:
96
- - `paths.workspace` ... workspace path
97
- - `paths.icloudBackup` ... iCloud offsite destination
98
- - `backup.keep` ... retention days (default: 7)
99
- - `backup.includeSecrets` ... whether to include `~/.ldm/secrets/`
100
- - `org` ... used for tar filename prefix
99
+ The complete engine suite writes only beneath unique disposable fixture roots:
100
+
101
+ ```bash
102
+ npm run test:backup-engine
103
+ ```
101
104
 
102
- ## Logs
105
+ The suite injects home, LDM, OpenClaw, Claude, Codex, workspace, destination, state, iCloud substitute, and lock paths. It aborts if any path escapes the fixture root.
103
106
 
104
- `~/.ldm/logs/backup.log` (LaunchAgent stdout/stderr)
107
+ ## Current tier boundary
105
108
 
106
- ## Technical Details
109
+ This Slice 2 engine implements internal-tier correctness only. Vault, iCloud, monitoring, restore, migration, and live schedule resumption remain separately gated. Do not run a real backup until those gates are approved.
107
110
 
108
- See [TECHNICAL.md](./TECHNICAL.md) for config schema, LaunchAgent plist, rotation logic, and script internals.
111
+ See [TECHNICAL.md](./TECHNICAL.md) for schemas and engine details.
@@ -1,112 +1,129 @@
1
1
  # Backup: Technical Details
2
2
 
3
- ## Config Schema
4
-
5
- All backup settings are in `~/.ldm/config.json`. The backup script reads these at runtime.
6
-
7
- ```json
8
- {
9
- "org": "wipcomputerinc",
10
- "paths": {
11
- "workspace": "~/wipcomputerinc",
12
- "ldm": "~/.ldm",
13
- "claude": "~/.claude",
14
- "openclaw": "~/.openclaw",
15
- "icloudBackup": "~/Library/Mobile Documents/com~apple~CloudDocs/wipcomputerinc-icloud/backups"
16
- },
17
- "backup": {
18
- "keep": 7,
19
- "includeSecrets": false
20
- }
21
- }
22
- ```
3
+ ## Installed components
23
4
 
24
- | Key | Type | Default | Description |
25
- |-----|------|---------|-------------|
26
- | `paths.workspace` | string | required | Root workspace directory to back up |
27
- | `paths.icloudBackup` | string | optional | iCloud destination for offsite copies |
28
- | `backup.keep` | number | 7 | Days of backups to keep before rotation |
29
- | `backup.includeSecrets` | boolean | false | Whether to include `~/.ldm/secrets/` |
30
- | `org` | string | required | Used as prefix in iCloud tar filenames |
31
-
32
- ## Script Location
33
-
34
- - **Source:** `scripts/ldm-backup.sh` in the wip-ldm-os-private repo
35
- - **Deployed to:** `~/.ldm/bin/ldm-backup.sh`
36
- - **Deployed by:** `deployScripts()` in `bin/ldm.js`, called during both `ldm init` and `ldm install`
37
- - **Restore script:** `scripts/ldm-restore.sh` deployed to `~/.ldm/bin/ldm-restore.sh`
38
-
39
- All `.sh` files in the repo's `scripts/` directory are deployed to `~/.ldm/bin/` on every `ldm install`. This means script fixes land automatically on the next update without requiring a full `ldm init`.
40
-
41
- ## LaunchAgent
42
-
43
- **Label:** `ai.openclaw.ldm-backup`
44
- **Plist source:** `shared/launchagents/ai.openclaw.ldm-backup.plist`
45
- **Deployed to:** `~/Library/LaunchAgents/ai.openclaw.ldm-backup.plist`
46
-
47
- ```xml
48
- <key>StartCalendarInterval</key>
49
- <dict>
50
- <key>Hour</key>
51
- <integer>3</integer>
52
- <key>Minute</key>
53
- <integer>0</integer>
54
- </dict>
55
- ```
5
+ | Source | Installed path | Purpose |
6
+ |---|---|---|
7
+ | `scripts/ldm-backup.sh` | `~/.ldm/bin/ldm-backup.sh` | Stable shell entry point |
8
+ | `scripts/ldm-backup-engine.py` | `~/.ldm/bin/ldm-backup-engine.py` | Policy, planning, capture, validation, state, and retention engine |
9
+ | `scripts/ldm-backup-policy.json` | `~/.ldm/bin/ldm-backup-policy.json` | Canonical source and exclusion policy |
10
+
11
+ The package bin manifest owns all three files. `ldm install` deploys code only. It does not run a backup, create a destination, change the LaunchAgent, enable a schedule, or write a containment marker.
12
+
13
+ The engine runtime is Python 3 standard library only. The shell entry point checks for `python3` and emits the structured failure code `python3-missing` before execution when it is unavailable. No pip package or third-party Python dependency is introduced.
14
+
15
+ ## Canonical policy schema
16
+
17
+ Policy schema version 1 contains:
18
+
19
+ - `rule_version`
20
+ - Explicit `evidence_status_values`
21
+ - Disk floors and start reserve
22
+ - Source ID, root, relative path, safe display path, destination, type, classification, required flag, and action
23
+ - Block reason when encryption or reconstruction proof is missing
24
+ - Exclusion pattern, reason, reconstruction source, and rule version
25
+
26
+ Legal actions are `capture`, `blocked`, `manifest`, and `exclude`. Legal types are `file`, `tree`, and `sqlite`. The placeholder string `complete-or-deferred-after-write-failure` is rejected by policy and runtime state validation.
27
+
28
+ The production policy has an explicit ID for every controlling Slice 1 source row. A fixture asserts exact set equality, not subset membership, so either an omission or an unreviewed addition fails loudly.
29
+
30
+ `manifest` requires a named evidence generator. The implemented `path-inventory` generator records the source type, exact members, member count, logical bytes, and SHA-256 of the serialized inventory. A missing source, generator failure, malformed record, or missing required evidence prevents completion. Required evidence without a complete Slice 2 generator uses `blocked`, including repo-aware Git refs and worktree state, macOS system facts, package inventories, and out-of-band 1Password bootstrap proof.
31
+
32
+ ## Planning and dry run
33
+
34
+ Planning resolves the configured roots, expands each policy entry, selects files using its exclusion rules, and computes checksums and logical sizes. `estimated_new_bytes` is the exact logical payload plus the serialized manifest and completion marker for an unchanged source set. The disk reserve is evaluated against that plan-time value.
56
35
 
57
- The plist uses `{{HOME}}` placeholders that are replaced at deploy time by `ldm init`.
36
+ `--dry-run` stops after planning and preflight. It does not acquire the persistent lock or call any atomic writer. JSON output is available with `--dry-run --json` for reviewers and tests.
58
37
 
59
- **Logs:** stdout and stderr both go to `~/.ldm/logs/backup.log`.
38
+ Configuration loading is fail-closed. A missing config permits documented defaults. An unreadable file, malformed JSON, a non-object root, or non-object `paths` or `backup` field produces `config-invalid`.
60
39
 
61
- **No Full Disk Access (FDA):** The LaunchAgent runs at 3:00 AM without FDA. Some paths (like `~/Library/Messages/`) are inaccessible without FDA. The target is to move the trigger to midnight via LDMDevTools.app (which has FDA) once the PID error is resolved.
40
+ ## Mutation-during-run contract
62
41
 
63
- ### Dead Triggers (Cleaned Automatically)
42
+ Planning is not treated as a filesystem freeze. Immediately before each source capture, the engine rebuilds that source observation from the same canonical policy. Plan-to-capture changes are allowed and recorded through planned size, modification time, checksum, capture-time size, capture-time modification time, destination checksum, and `changed_since_plan`.
64
43
 
65
- The `cleanDeadBackupTriggers()` function in `ldm.js` removes old competing triggers on every `ldm init`:
66
- - Old cron entries referencing `LDMDevTools.app`
67
- - `com.wipcomputer.daily-backup` LaunchAgent
68
- - OpenClaw `backup-verify` cron entries
44
+ For regular files and trees, the capture-time observation is the source correspondence proof. Destination bytes are compared to that observation, and the manifest records per-source member counts, payload bytes, and inventory fingerprints for both planning and capture. After copying the full source, the engine observes it again. Any member addition, removal, type change, size change, modification-time change, or checksum change during capture fails with `source-changed-during-capture`. Same-size content changes are therefore detected. A plan-time change does not force failure merely because an active source changed before its capture began.
69
45
 
70
- Only `ai.openclaw.ldm-backup` should exist.
46
+ SQLite uses a separate contract. Python's SQLite backup API produces a transactionally consistent destination snapshot. The manifest labels these records `sqlite-backup-api-transaction-snapshot`; it does not claim that the destination checksum equals a raw live database file checksum.
71
47
 
72
- ## Rotation Logic
48
+ Before copying each source, the engine refreshes size and modification-time estimates for every capture source still pending without hashing each remaining tree. It then performs the full member and checksum observation for the source about to be copied. The engine recalculates the reserve from bytes already staged, refreshed remaining payload, projected manifest and completion metadata, current available space, and the required reserve. Metadata projection updates only changed source and evidence records instead of rebuilding the complete manifest for every source. A failed recheck raises `capture-reserve-gate` before the changed source is copied and records the updated estimate and reserve in `result.json`.
73
49
 
74
- The backup script handles rotation after a successful backup:
50
+ Manifest actions are gated before any source path is inspected or walked. An entry without an implemented generator is reported as blocked immediately. This prevents generator-less entries from hashing large trees or failing on special files that the unused source path happens to contain.
75
51
 
76
- 1. List all dated directories in `~/.ldm/backups/` (format: `YYYY-MM-DD--HH-MM-SS`)
77
- 2. Sort by name (which sorts chronologically)
78
- 3. Skip any directory containing a `.pinned` marker file
79
- 4. Delete directories beyond the `keep` count (oldest first)
80
- 5. Same rotation logic applies to iCloud tars at `paths.icloudBackup`
52
+ ## Run lifecycle
81
53
 
82
- **Pinning:** `ldm backup --pin "reason"` creates a `.pinned` file in the latest backup directory. Pinned backups are never rotated.
54
+ 1. Create the lock with `O_CREAT | O_EXCL`.
55
+ 2. Reject an active lock. Report a dead-PID lock as stale without replacing it.
56
+ 3. Build the plan from the canonical policy.
57
+ 4. Atomically write `schedule.json` and `attempt.json` before staging.
58
+ 5. Fail preflight before any staging directory when a required gate, destination, or reserve check fails.
59
+ 6. Create `.in-progress-<snapshot-id>`.
60
+ 7. Generate and validate required manifest evidence.
61
+ 8. Re-observe each capture source and all remaining capture sources.
62
+ 9. Recalculate the disk reserve before copying the current source.
63
+ 10. Capture SQLite databases with Python's SQLite backup API and run `PRAGMA quick_check` on each destination.
64
+ 11. Re-observe regular-file and tree sources after capture and fail if they changed during capture.
65
+ 12. Atomically write `manifest.json`.
66
+ 13. Validate every required capture record and manifest-evidence record.
67
+ 14. Atomically write `completion.json` last.
68
+ 15. Verify completion proof, then atomically promote staging.
69
+ 16. Write `result.json` and `success-local.json`.
70
+ 17. Rotate only eligible verified snapshots.
83
71
 
84
- ## iCloud Offsite Details
72
+ Failures do not promote staging and do not print `Backup complete`. A process crash can leave only an unmistakable in-progress directory and stale lock.
85
73
 
86
- After the local backup completes:
74
+ ## Manifest fields
87
75
 
88
- 1. Tar + gzip the entire dated backup directory
89
- 2. Filename format: `<org>-<machine>-<timestamp>.tar.gz`
90
- 3. Copy to `paths.icloudBackup` (from config.json)
91
- 4. Apply the same rotation (keep N, skip pinned)
92
- 5. iCloud syncs the file to all devices automatically
76
+ Each captured member records:
93
77
 
94
- The iCloud path must exist. The script does not create it. `ldm init` does not create it either. Create it manually if it does not exist.
78
+ - Source ID and resolved source path
79
+ - Relative destination
80
+ - Type
81
+ - Logical size
82
+ - Modification time in nanoseconds
83
+ - SHA-256 checksum
84
+ - Plan-time size, modification time, and checksum where meaningful
85
+ - Whether the source changed between planning and capture
86
+ - The source consistency contract
95
87
 
96
- ## SQLite Safety
88
+ The manifest also records schema version, UTC snapshot ID, tool version, policy rule version, timestamps, planned and captured payload bytes, required manifest evidence, exclusions, and terminal result. Each exclusion records source ID, path or pattern, reason, reconstruction source, rule version, and matched count.
97
89
 
98
- SQLite files are backed up using `sqlite3 .backup`, not `cp`. This ensures a consistent snapshot even if the database is being written to. The script checks for the `sqlite3` binary and skips database backup with a warning if it is not found.
90
+ `completion.json` records snapshot ID, `status: complete`, completion time, and manifest checksum. Its presence alone is insufficient unless the manifest checksum and terminal result validate.
99
91
 
100
- Files backed up this way:
101
- - `~/.ldm/memory/crystal.db`
102
- - `~/.openclaw/memory/main.sqlite`
103
- - `~/.openclaw/memory/context-embeddings.sqlite`
92
+ ## Structured state
104
93
 
105
- ## Excludes
94
+ All state files use same-directory temporary files, `fsync`, and atomic rename.
95
+
96
+ | File | Meaning |
97
+ |---|---|
98
+ | `schedule.json` | Schedule ID, timezone, expected slot, containment status, and marker-derived evidence status or `unknown` |
99
+ | `attempt.json` | Run ID, PID, tool version, start time, and preflight values |
100
+ | `result.json` | Terminal result, failure code, estimates, and per-tier status |
101
+ | `success-local.json` | Last verified internal success |
102
+ | `success-vault.json` | Reserved for Slice 3 |
103
+ | `success-icloud.json` | Reserved for Slice 4 |
104
+
105
+ These records are diagnostic. They do not replace snapshot completion proof.
106
+
107
+ ## Retention
108
+
109
+ Retention considers a directory eligible only when:
110
+
111
+ - It is not named `.in-progress-*`.
112
+ - `manifest.json` has `terminal_result: success`.
113
+ - `completion.json` has `status: complete`.
114
+ - The completion marker matches the manifest SHA-256.
115
+ - `.pinned` is absent.
116
+
117
+ The engine keeps the configured number of unpinned eligible snapshots plus every pinned snapshot. Unverified directories are preserved for later disposition.
118
+
119
+ ## Fixture isolation
120
+
121
+ Tests set `LDM_BACKUP_REQUIRE_FIXTURE=1` and provide a unique `LDM_BACKUP_FIXTURE_ROOT` whose basename starts with `ldm-backup-fixture-`. Before planning or writing, the engine proves that all injected home, LDM, OpenClaw, Claude, Codex, workspace, destination, state, iCloud substitute, and lock paths resolve beneath that root.
122
+
123
+ The entire fixture suite runs with:
124
+
125
+ ```bash
126
+ npm run test:backup-engine
127
+ ```
106
128
 
107
- The workspace tar excludes:
108
- - `node_modules/` ... reconstructable via npm install
109
- - `.git/objects/` ... reconstructable via git fetch
110
- - `backups/` ... avoids recursive backup
111
- - `_trash/` ... already deleted content
112
- - `*.tar.gz` ... avoids backing up old backup archives
129
+ It covers success, dry-run parity, exact policy IDs, required manifest evidence and generator failures, generator-less manifest short-circuiting over a tree containing a Unix socket, plan-to-capture size and same-size changes, capture-time reserve success and refusal, fatal during-capture mutation, concurrent and stale locks, missing and blocked required sources, SQLite failure, simulated ENOSPC, preflight failure, crash staging, atomic state, completion proof, configuration failures, UTC IDs, evidence status, retention, and path escape refusal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/wip-ldm-os",
3
- "version": "0.4.87-alpha.1",
3
+ "version": "0.4.87-alpha.2",
4
4
  "type": "module",
5
5
  "description": "LDM OS: identity, memory, and sovereignty infrastructure for AI agents",
6
6
  "engines": {
@@ -18,7 +18,7 @@
18
18
  "scripts": {
19
19
  "build:bridge": "cd src/bridge && npm install && npx tsup core.ts mcp-server.ts cli.ts openclaw.ts --format esm --dts --clean --outDir ../../dist/bridge",
20
20
  "build": "npm run build:bridge",
21
- "prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest && npm run test:install-prompt-policy && npm run test:readme-install-prompt && npm run test:ldm-status-timeout && npm run test:ldm-status-concurrency && npm run test:legacy-npm-sources-migration",
21
+ "prepublishOnly": "npm run build:bridge && npm run validate:bin-manifest && npm run test:install-prompt-policy && npm run test:readme-install-prompt && npm run test:ldm-status-timeout && npm run test:ldm-status-concurrency && npm run test:legacy-npm-sources-migration && npm run test:ldm-install-bin-shim && npm run test:backup-engine",
22
22
  "validate:bin-manifest": "node scripts/validate-bin-manifest.mjs",
23
23
  "test:skill-frontmatter": "node scripts/test-skill-frontmatter.mjs",
24
24
  "test:install-prompt-policy": "node scripts/test-install-prompt-policy.mjs",
@@ -51,7 +51,8 @@
51
51
  "test:doctor-hook-dedupe": "node scripts/test-doctor-hook-dedupe.mjs",
52
52
  "test:boot-dir-truth": "node scripts/test-boot-dir-truth.mjs",
53
53
  "test:deploy-hook-ownership": "node scripts/test-deploy-hook-ownership.mjs",
54
- "test:doctor-commit-deployed": "node scripts/test-doctor-commit-deployed.mjs"
54
+ "test:doctor-commit-deployed": "node scripts/test-doctor-commit-deployed.mjs",
55
+ "test:backup-engine": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-backup-engine.py"
55
56
  },
56
57
  "claudeCode": {
57
58
  "hook": {
@@ -73,6 +74,17 @@
73
74
  "source": "scripts/ldm-backup.sh",
74
75
  "purpose": "daily backup; LaunchAgent at 03:00"
75
76
  },
77
+ {
78
+ "name": "ldm-backup-engine.py",
79
+ "source": "scripts/ldm-backup-engine.py",
80
+ "purpose": "policy-driven backup engine"
81
+ },
82
+ {
83
+ "name": "ldm-backup-policy.json",
84
+ "source": "scripts/ldm-backup-policy.json",
85
+ "executable": false,
86
+ "purpose": "canonical backup source policy"
87
+ },
76
88
  {
77
89
  "name": "ldm-restore.sh",
78
90
  "source": "scripts/ldm-restore.sh",