Experiment/store - #132
Draft
adrianliechti wants to merge 32 commits into
Draft
Conversation
Conflict resolution: - keep chatstore wiring (server.go) alongside main's new skills/notebook library handlers - keep session bootstrap + PIN-unlock reload in useChats - take main's versions of package.json, index.css, and the text-[10px] styling tweaks superseded by main's refactor (drops the experimental mobile root-font scaling) - dedupe wingman proxy consts in vite.config.ts (both sides added identical code) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n tab focus - applyRemoteEvents: an init snapshot may legitimately arrive with a seq gap after another device compacted the chat (server dropped the log prefix). Decrypt before the gap check, accept init frames as resets, and rewrite the local log instead of appending. Previously the device was permanently wedged: every pull hit the gap and bailed, every post 409'd into PostConflictAfterRetry. - deleteChat: delete the chat's blobs on the server too (ids are unique per attachment, never shared across chats) instead of leaking them. - useChats: re-pull on tab visibility (throttled 30s, skipped while a save is pending) so a second device sees new chats without a reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old flag-based check refused to upload local chats if the server had any — stranding a second device's local-only chats — and didn't even achieve that: hydrateFromOpfs persists legacy chats as pending targets, so the next boot's flushPending uploaded them anyway. Now: union by chat id (ids are UUIDs, so unknown ids upload safely); on id collision the newer side wins by updated timestamp, matching the sync engine's conflict policy. Blobs of hydrated targets are pushed before their events — previously only saveChat uploaded blobs, so migrated chats lost attachments on other devices. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vp test is broken at the current version pairing (vite-plus 0.2.1 expects a vitest bin that @voidzero-dev/vite-plus-test 0.1.24 does not register), so "npm test" invokes the package's CLI entry directly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vitest resolves the vite config with a placeholder outDir; the plugin's closeBundle hook copied pdfjs assets into it on every test run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers auth (401, stable hashed identity), keystore CAS lifecycle (create-only, stale/current etag), event append/read (NDJSON, fromSeq, 409 on stale seq, 400s), idempotent dedup, compaction (prefix drop, head preserved), chat/blob lifecycle, cross-user isolation, and the 1 MiB frame cap. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New frames are written as 0x02 || kid || nonce || ct so a future DEK rotation can route decryption by key id instead of guess-decrypting. Legacy headerless frames still decrypt (GCM auth makes the fallback parse safe); version-1 keystores imply kid 1. Runtime is still single-key — rotation becomes a data-format non-event later. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ChatSync now publishes activity (syncing, pending count, last sync, last error) and emits the fresh chat list when a pull applies remote changes; useChats adopts that list so manual/focus syncs update the sidebar. SyncSection shows the activity line with a Sync now button and surfaces sync errors that previously only hit the console. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chats keep the fine-grained event-log model; everything else in OPFS
(agents, skills, notebooks, repositories, images, profile.json) now
syncs as encrypted files through a new FileSync engine.
Server: GET /v1/files listing plus GET/PUT/DELETE /v1/files/{id} with
ETag CAS, backed by a per-user manifest in the file provider. Files are
addressed by sha256(path); the real path travels only inside the
encrypted envelope, so the server never learns the tree structure.
Client: opfs-core now emits mutation events for every write/delete;
FileSync tracks them (excluding chats/ and _sync/), debounces pushes,
and does a full reconcile at session start and on manual sync. Conflicts
resolve last-write-wins by file mtime, including the fresh-device case
where both sides have unsynced copies of the same path. Files over
64 MiB are skipped with a warning.
SyncSection merges chat and file activity into one status row; the
crypto layer gains a file envelope (header + raw bytes in one AES-GCM
message) and blob encryption drops its base64 round-trip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Collection indexes ({collection}/index.json) merge by entry id instead
of last-write-wins — two devices creating agents/notebooks in parallel
no longer clobber each other's index entries. After a clean full
reconcile, entries whose backing item vanished are pruned, so deletions
converge too instead of resurrecting through the merge.
The session now owns the polling cadence (online-first): chats pull
every minute while the tab is visible and immediately on focus; the
file tree reconciles every five minutes. useChats drops its own
visibility handler and just renders what the engine delivers, skipping
mid-stream swaps.
New always-visible sync indicator in the top bar (cloud check / upload
/ spinner / amber alert or lock) backed by a shared useSyncStatus hook
that folds session state and both engines' activity into one health
value; clicking it opens the sync settings. SyncSection reuses the
same hook.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It stores the whole encrypted workspace (chats, files, keystore), not just chats. pkg/chatstore → pkg/store, pkg/server/chatstore → pkg/server/store, CHATSTORE_* env vars → STORE_* (default path ./data/store), config key "chatstore" → "store", chatstoreClient.ts → storeClient.ts, chatSession.ts → storeSession.ts. The chat-specific engine keeps its chatSync name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
subscribeSession invokes the callback synchronously with the current state; if the session was already in error/disabled, the callback hit unsub before its const initializer ran. Surfaced when the store API 404'd (server running without STORE_ENABLED) and every whenReady call crashed instead of rejecting cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A WINGMAN_URL exported in the shell pointed the vite proxy at a remote inference server, bypassing task serve and its store routes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… env Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A first-sync snapshot of a long chat (or one message with large tool results) exceeded the server's 1 MiB per-frame cap and died in the line scanner as '400: token too long'. The client now splits deltas into hash-chained frames of ~4 MiB plaintext each, the server cap is 8 MiB (scanner overflows now answer 413 instead of 400), and compaction keeps the whole snapshot by compacting at the batch's first seq rather than the head. Workspace file sync cap raised 64 → 256 MiB for large office documents and notebook media; chat-attachment blobs were never capped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Chunking cannot split a single log entry, so one message with megabytes of pasted text or tool results needs the headroom; normal frames stay at ~4 MiB plaintext. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The mirror (log.jsonl, target.json) is plaintext locally — the PIN only protects the server copies — but loadAll blocked on whenReady, so a locked session showed an empty chat list until unlock. Server-mode loading is now session-driven: mirror while locked or errored, pull on ready. Also coalesce concurrent pull() calls; the old flow ran two racing pulls at unlock (initial load resuming plus the ready subscription), which could double-apply events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nothing deployed uses the old formats, so: frames must carry the versioned header (no headerless fallback), keystores are version 2 with a required kid, legacy chat.json promotion is gone, and the migrateToServer reconciliation module is deleted — crash-recovered pending targets flush through the normal save path, which storeSession now kicks directly on ready. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chat.json is what today's deployments write when the store is off; a deployment that turns the store on must sweep that data up. Promotion lives in hydrateFromOpfs as before, but blob upload now happens inline on a chat's first flush instead of via the deleted migration module. The truly never-deployed compat (headerless frames, keystore v1) stays removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also self-heal when the head claims synced but the local history file is missing (cleared cache, lost file): forget the head so the next pull refetches from seq 0 instead of leaving the chat invisible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Data loss / corruption: - flushChat clears the pending target only if a newer save didn't replace it mid-flight; flushPending re-runs when work arrived during the flush and no longer aborts the whole loop on one failing chat - saveChat persists locally before any network I/O (blob uploads moved into flushChat), so offline saves with attachments survive - replayLog's init entry now resets state — compaction snapshots applied over a baseline no longer duplicate every message - per-chat operation lock serializes pull-applies, flush posts, and compaction on the same history; the 409 recovery path uses the in-lock variant; already-applied events are skipped instead of misread as gaps; concurrent maybeCompact can no longer clobber freshly appended entries when rewriting the local history Server: - the head file is now the append commit point (seq + committed size): uncommitted orphan lines from a failed batch are invisible to readers and truncated before the next append, instead of permanently forking the seq/hash chain for every device - storage identity uses the same normalized value as /me (trim + email fallback) — a padded or whitespace-only X-Forwarded-User no longer splits one user across directories or funnels several users into one Resilience: - deletions are journaled in sync state and retried until the server confirms; pulls skip journaled deletions instead of resurrecting them - session init errors retry every 15s instead of disabling sync for the tab's lifetime; saves during locked/error sessions stash a local pending target instead of hanging or rejecting - a single log entry too large for any frame fails fast with a clear persistent error instead of hammering the server with 413s Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…into experiment/store
adrianliechti
force-pushed
the
main
branch
2 times, most recently
from
July 8, 2026 22:05
d13d26e to
ff36f5e
Compare
adrianliechti
force-pushed
the
main
branch
3 times, most recently
from
July 16, 2026 21:16
eb3f1de to
b8556ac
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.