First PR of E2-β (offline write queue). Adds the durable, append-only
log the upcoming β.2 (adapter push hook) and β.3 (replayer) build on.
Standalone in this PR — nothing in the main flow consumes it yet, so
the surface stays unit-tested but unwired.
Pieces:
- `plugin/src/offline/OfflineQueue.ts`
- JSONL log under `<dir>/log.jsonl`. Two record kinds:
`{type:"op",id,ts,op}` and `{type:"completed",id,ts}`.
- Replays the log on `open()` to seed the in-memory pending set:
every op without a matching tombstone becomes a pending entry.
- `enqueue(op)` appends + assigns a monotonic id; rejects when the
in-memory byte total would cross the configured cap (default
500 MB).
- `markCompleted(id)` appends a tombstone and triggers compaction
when the on-disk log has grown past 2× the live pending bytes.
- `compact()` rewrites the log with only pending entries (atomic
via tmp + rename, with a Windows-friendly copy+unlink fallback).
- `clear()` drops everything.
- `pending()` / `stats()` for the upcoming UI hooks.
- All ten op kinds the adapter exposes are representable
(`write`, `writeBinary`, `append`, `appendBinary`, `mkdir`,
`remove`, `rmdir`, `rename`, `copy`, `trashLocal`); binary
payloads are base64-encoded so the JSONL stays line-delimited.
- `plugin/tests/OfflineQueue.test.ts` — 12 cases:
empty start; enqueue id ordering; markCompleted; cross-instance
persistence (with + without completions); monotonic id stream
preserved across reopen; compaction shrinks the log; clear()
empties + persists empty; cap enforcement (rejected enqueue
doesn't consume an id slot); malformed-line tolerance; every op
kind round-trips verbatim.
All 392 unit tests pass (380 prior + 12 new).
Next: β.2 wires the adapter's reconnecting state to push into the
queue, β.3 builds the replayer, β.4 surfaces "N pending edits" in
the StatusBar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>