feat(sync): write-through Brain.correct() → /api/v1/ingest (day 3 of #194)#200
Conversation
…194) Every Brain.correct(draft, final) now enqueues the correction into a local sync_queue table and a background daemon thread drains the queue by POSTing each row to api.gradata.ai/api/v1/ingest. Removes the dependency on session-end hooks and the cron sync band-aid. See /home/olive/gradata-office-hours-memo.md for the architectural rationale. ## Components - src/gradata/_sync_queue.py: enqueue/peek/mark_synced/mark_failed CRUD + enqueue_correction helper - src/gradata/_sync_worker.py: daemon-thread SyncWorker with start/stop(drain)/_tick, handles 2xx + dedup + 422 poison + 429 backoff + 5xx + network - src/gradata/brain.py: Brain.__init__ starts worker when API key resolvable AND GRADATA_DISABLE_WRITE_THROUGH != 1; Brain.correct enqueues post-local-write; Brain.close drains and stops - src/gradata/_migrations/__init__.py + onboard.py: sync_queue table + idx_sync_queue_pending (idempotent, no-op if #195 lands first) ## Env vars - GRADATA_API_KEY (existing): write-through requires it - GRADATA_DISABLE_WRITE_THROUGH=1: opt-out, fall back to hook+cron path - GRADATA_CLOUD_INGEST_URL (default https://api.gradata.ai/api/v1/ingest) - GRADATA_SYNC_TICK_SEC (default 30) ## Failure modes - Cloud unreachable: row stays pending, retried next tick - 422 permanent: poison row marked synced + failed so it doesn't block batch - 429: bail batch, retry next tick - No API key: worker doesn't start, no enqueue, local correct() unaffected - _write_through_enqueue exception: caught, local correct() always succeeds ## Tests - tests/test_sync_queue.py (9): existing CRUD primitives + enqueue_correction - tests/test_sync_worker.py (9): HTTPServer stub /ingest with all status branches + at-least-once + stop-drain - tests/test_brain_write_through.py (5): enqueue happy path, disabled, no-key, cloud-unreachable, sabotaged enqueue 38 passed locally. Series: #195 (day 1 SDK queue), Gradata/gradata-cloud#58 merged (day 2 cloud receiver), this PR (day 3 wire-up).
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (9)
📝 Walkthrough
WalkthroughThis PR implements write-through cloud sync for Brain corrections. A new ChangesWrite-through cloud sync
Sequence Diagram(s)Write-through sync workflow from correction to cloud delivery: sequenceDiagram
participant User
participant Brain
participant SyncWorker as SyncWorker<br/>(daemon)
participant Queue as sync_queue<br/>(SQLite)
participant Cloud
User->>Brain: brain.correct(draft, final)
Brain->>Brain: local correction logic
Brain->>Queue: enqueue_correction(brain_id, correction, event_id)
Queue-->>Brain: row_id
Note over Brain,Queue: enqueue best-effort, errors logged
Note over SyncWorker: background tick loop
SyncWorker->>Queue: peek_pending(limit=50)
Queue-->>SyncWorker: list of pending rows
loop for each row
SyncWorker->>Cloud: POST /api/v1/ingest<br/>payload + bearer token
alt 2xx success
Cloud-->>SyncWorker: 200/201
SyncWorker->>Queue: mark_synced(id)
else 429 rate limit
Cloud-->>SyncWorker: 429
SyncWorker->>Queue: mark_failed(id, error)
SyncWorker->>SyncWorker: abort tick, sleep
else 422 permanent
Cloud-->>SyncWorker: 422
SyncWorker->>Queue: mark_failed then mark_synced
else 5xx/network
Cloud-->>SyncWorker: 500 or timeout
SyncWorker->>Queue: mark_failed(id, error)
Note over SyncWorker,Queue: continues batch, retries next tick
end
end
User->>Brain: brain.close()
Brain->>SyncWorker: stop(drain=True)
SyncWorker->>SyncWorker: final synchronous tick
SyncWorker->>Brain: join thread
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Suggested labels
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 OpenGrep (1.20.0)OpenGrep fatal error (exit code 2): �[32m✔�[39m �[1mOpengrep OSS�[0m �[1m Loading rules from local config...�[0m Comment |
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
…ve (day 4 of #194) Brain.correct() write-through (PR #200) is now the default cloud sync path. The session_close hook's legacy cloud_sync_tick was double-writing every correction. Default behavior: session_close skips cloud_sync_tick. Lesson graduation, pipeline, tree consolidation, lesson_applications resolution all still run as before — only the cloud sync portion is gated. Opt-out: GRADATA_DISABLE_WRITE_THROUGH=1 restores the legacy hook-based cloud sync path AND disables write-through. The two paths are mutually exclusive: only one ever runs. Untouched: - daemon /sync (dashboard 'Sync Now' button) - /api/v1/sync (bulk endpoint, used by Brain.close() drain) - /home/olive/.gradata/sync_cron.py (safety-net cron, keep for 7-day soak) Follow-up: after 7 days of clean write-through telemetry in cloud, the cron and legacy /api/v1/sync code paths can be removed in a separate PR.
…ve (day 4 of #194) (#201) Brain.correct() write-through (PR #200) is now the default cloud sync path. The session_close hook's legacy cloud_sync_tick was double-writing every correction. Default behavior: session_close skips cloud_sync_tick. Lesson graduation, pipeline, tree consolidation, lesson_applications resolution all still run as before — only the cloud sync portion is gated. Opt-out: GRADATA_DISABLE_WRITE_THROUGH=1 restores the legacy hook-based cloud sync path AND disables write-through. The two paths are mutually exclusive: only one ever runs. Untouched: - daemon /sync (dashboard 'Sync Now' button) - /api/v1/sync (bulk endpoint, used by Brain.close() drain) - /home/olive/.gradata/sync_cron.py (safety-net cron, keep for 7-day soak) Follow-up: after 7 days of clean write-through telemetry in cloud, the cron and legacy /api/v1/sync code paths can be removed in a separate PR. Co-authored-by: data-engineer <data-engineer@gradata.ai>
Day 3 of #194 — wire Brain.correct() to write-through cloud sync.
What
Every Brain.correct(draft, final) now enqueues into local sync_queue + a background daemon thread POSTs each row to api.gradata.ai/api/v1/ingest.
Why
Removes the dependency on session-end hooks (which barely fire for long-running agents on hermes --continue) and the cron sync band-aid. See /home/olive/gradata-office-hours-memo.md for the full architectural rationale.
Env vars
Failure modes
Tests
38 passed (test_sync_queue × 9, test_sync_worker × 9, test_brain_write_through × 5, test_brain × 15 regression).
Series