docs(map): cross-device sync rewrite, fixed defects and what is still local
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 50s
ci / 모바일 린트·타입·Jest (push) Successful in 38s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 21s
ci / .NET API 서버 테스트 (push) Successful in 11s
deploy-site / deploy (push) Successful in 33s
ci / 워크스페이스 빌드 검증 (push) Has been cancelled

This commit is contained in:
Yun Chan 2026-09-27 14:04:49 +09:00
parent 53fcdf6305
commit 6b06442412
7 changed files with 45 additions and 18 deletions

View file

@ -92,6 +92,14 @@ Legend: `[ ]` open · `[~]` in progress · `[!]` blocked externally · `[x]` res
| GAP-LLM-04 | LLM | `[~]` **Gemma/Ollama residency·burst·unbounded chat root cause (2026-09-22).** 19:14:12 boot warmup의 `keep_alive: 30m`가 `gemma4:e4b`를 expiry 19:44:12까지 VRAM 3,226,342,521 bytes / context 4096으로 상주시켰다. Windows GPU Engine PID 표본에는 활성 Ollama compute가 없어 무한 추론이 아니라 강제 residency였다. 19:11:17–19:11:58 자동 제안 반복은 기존 900 ms / 12 per min / 5 candidates / 128 tokens / 1-char growth가 허용한 burst였다. 위험 경로는 chat의 무제한 `num_predict`(Ollama 기본 `-1`), timeout·외부 abort 부재, 공유 취소, `done` 없는 EOF 성공, voice cancel 미전파, watchdog의 비취소였다. | `apps/desktop/src/main/services/{LocalLLMService,SuggestionService,VoiceConversationService}.ts`, Ollama `/api/ps` and Windows GPU Engine observations | 구현 계약: boot warmup 제거, suggestion `keep_alive: 2m`; 600 ms debounce / 5 s interval / 6 per min (hard max 12) / 3 candidates / 64 tokens / 12-char growth / 8 s timeout; 요청별 cancellation, bounded requests, done/cleanup, voice single-flight, spawn/poll dedupe와 dispose. Raw Ollama 증거: cold bounded 요청은 client hard timeout 15.044 s에 취소된 뒤 `/api/ps` empty와 `/api/version` 80 ms 회복을 보였고, explicit warmup HTTP 200은 16.639 s, 후속 `num_predict=1` / `keep_alive='2m'` 요청은 553 ms HTTP 200 / `done:true` / `eval_count:1` / `response=OK` / `done_reason:length`였으며 `/api/ps` expiry는 약 119.9 s였다. 이어 19:48:59 +09:00에는 새 generate/unload/kill/retry 없이 충분히 지난 뒤 단일 `/api/ps`가 HTTP 200 / 45.8 ms / `{models:[]}`였고 `/api/version`은 HTTP 200 / 7.3 ms / `0.32.13`이었다. 이는 raw API 수준의 expiry 뒤 unload 확인이다. **남은 조건:** 앱 재시작·GUI·실제 자동제안 타이핑 증거는 아니므로 그 경로에서 rate limit, timeout/cancel, 2분 residency를 확인한다. |
| GAP-INPUT-06 | Input intelligence | `[~]` **제안 폭주 상한 및 runtime 확인.** 이전 24.7 s / 4.9 s, boot warmup, `keep_alive: 30m`, 48/96/128 tokens 및 2/5 candidates 기록은 역사적 가설/완화 이력이며 현재 정책이 아니다. 현 정책은 600 ms debounce, 5 s minimum interval, 6 requests/min default (hard max 12), 3 candidates, 64 output tokens, 12-character growth, 8 s timeout, `keep_alive: 2m` 및 boot warmup 제거다. | `apps/desktop/src/main/services/SuggestionService.ts`, `packages/core/src/input-intelligence.ts`, `LocalLLMService.ts` | 표적 자동 검증은 6 files / 69 passed / 0 failed이며 raw Ollama의 15.044 s cold timeout cleanup, 약 119.9 s residency expiry, 그리고 19:48:59 +09:00에 새 generate/unload/kill/retry 없이 확인한 `/api/ps` HTTP 200 / 45.8 ms / `{models:[]}` (`/api/version` HTTP 200 / 7.3 ms / `0.32.13`)도 확인됐다. 전체 strict typecheck는 green이 아니며, 이 raw API unload 증거는 앱 재시작·GUI·실제 자동제안 타이핑 검증이 아니다. 그 경로에서 rate limit, 8 s abort, 3-candidate/64-token 상한을 확인한다. |
| GAP-SYNC-01 | Sync | Desktop↔mobile sync was one-way and lossy. (1) `pushAll` set the cursor to *now* before `pullAll` ran, so the first pull after sign-in fetched nothing — mobile-created data never reached the desktop except via Realtime while running (which never connected, GAP-SYNC-02). (2) `_inArray` used only the first meeting id, so memos/documents of every other meeting were never pushed. (3) No deletion propagation either way. (4) Pull had no `user_id` filter — team meetings of other users landed in the personal DB and a desktop re-push cleared `team_id`. (5) One global, local-clock cursor across accounts; no pagination (1,000-row cap). (6) Desktop writes did not move `history.revision`, so mobile's optimistic concurrency silently overwrote desktop edits. (7) One case-duplicate dictionary word failed the whole batch. (8) Memo tags, custom commands, templates, favorites and history titles were not synced at all; anonymous local-mode data was never uploaded after sign-in. | `apps/desktop/src/main/services/sync/*`, `CloudSyncService.ts`, `server/supabase/migrations/20260927000034_cross_device_sync_parity.sql` | `[x]` 2026-09-27: engine rewritten (outbox + per-user server-clock keyset cursors + tombstones + per-row isolation + dictionary adoption + natural-key memo tags + client-keyed template RPCs + local-mode import). Evidence: `apps/desktop/tests/main/sync/SyncEngine.test.ts` (20) + `apps/desktop/tests/integration/cross-device-sync.supabase.test.ts` (5, local Supabase stack); full desktop suite 1499/1500 (1 = known caption env timeout); SQL checks for revision bump, tombstones, account-delete cascade. |
| GAP-SYNC-02 | Sync | **Desktop Realtime never connected.** Electron 33 main (Node 20.18) has no global `WebSocket`; supabase-js Realtime only ever logged `TIMED_OUT`/`CLOSED` (installed-app log 2026-09-16) and gave up after three retries. Also: filtered `postgres_changes` channels never deliver DELETE (Supabase limitation). | `CloudSyncService.ts`, `services/sync/realtime-transport.ts`, integration test "Realtime" case | `[x]` 2026-09-27: `ws` injected as the Realtime transport (`ws` now a declared desktop dependency, externalised in the main bundle); deletions arrive as `sync_tombstones` INSERT events; the heartbeat re-subscribes a channel that is not `joined`. Verified live against the local stack (INSERT/UPDATE + tombstone events received). |
| GAP-SYNC-03 | Portability | Mobile/cloud data export failed for any account with meetings or generated documents: `export_account_portability` used `to_jsonb(row)` while the v1 archive contract (server restore + mobile `exactKeys`) is a frozen key set, and later migrations added columns. | `server/supabase/migrations/20260927000034_cross_device_sync_parity.sql` §7 | `[x]` 2026-09-27: export projects each dataset onto its v1 keys; integration test asserts the memo/meeting key sets. |
| GAP-SYNC-04 | Mobile devices | Mobile `normalizeRegisteredDevice` rejected every platform except `android`/`ios`, so the first desktop/web device on an account would have made the whole Devices screen fail to load. | `apps/mobile-rn/src/features/devices/device-service.ts`, `screens/DevicesScreen.tsx` | `[x]` 2026-09-27: accepts the server platform list (`android/ios/web/windows/macos`), card shows the platform; `__tests__/devices.test.ts` 3/3, full mobile Jest 372/372. |
| GAP-SYNC-05 | Sync | **Not yet in production.** Migration `20260927000034` is applied only to the local stack; the rewritten desktop sync ships with the next desktop release. Until the migration is deployed, desktop tombstone pulls and template RPCs fail against production (they are isolated per step, other entities keep syncing). | migration file, `apps/desktop/src/main/services/sync/` | `[ ]` Deploy the migration (`supabase db push` against the production project) **before** publishing a desktop build that contains this engine; then run a GUI pass: sign in on desktop, edit/delete on the phone, confirm desktop updates within seconds and the phone lists the desktop under Devices. |
| GAP-SYNC-06 | Sync | Still desktop-local (not mirrored): knowledge base (desktop local RAG embeddings vs cloud pgvector — different embedding spaces), audio (desktop WAV files vs mobile Storage uploads), `user_settings` (locale, auto-polish, active command), desktop preset-command prompt edits, meeting `transcripts` segments (desktop uses `raw_transcript`, which mobile also fills). | `docs/map/10-feature-catalog.md` MEM-09/MEM-05/SHELL rows | `[ ]` Decide per item: knowledge docs could sync as source text and be re-embedded per surface; audio needs a size/quota policy before upload. |
| GAP-SYNC-07 | Sync | `sync_tombstones` grows without a schedule. `prune_sync_tombstones_v1(interval)` (service_role) exists; nothing calls it. | migration §1 | `[ ]` Call it from an existing cron (e.g. the Forgejo scheduled workflow) with the 180-day default. A client whose cursor is older than the retention must fall back to a full resync. |
---
## 2. Mobile checklist roll-up (from `MOBILE_APP_COMPLETION_SSOT.md` §4)
@ -203,6 +211,13 @@ Actionable checklist for the work started this session. Fields to fill are blank
## 7. 알려진 설계 제약 (수용됨 — 결함 아님)
### 7.x 기기 간 동기화 (2026-09-27 수용)
- **충돌 규칙은 "마지막 쓰기 우선 + 로컬 미전송 보호"다.** 데스크톱은 아직 올리지 않은 로컬 변경이 있는 행을 pull이 덮지 않고, 올릴 때는 서버 행을 통째로 덮는다. 모바일은 `revision`으로 충돌을 감지한다(데스크톱 쓰기도 이제 revision을 올린다). 필드 단위 병합은 하지 않는다.
- **원격 삭제가 로컬 미전송 수정보다 우선한다.** 다른 기기에서 지운 행은 데스크톱에서 편집 중이었어도 지워진다.
- **업그레이드 후 첫 동기화는 예전 삭제를 되살릴 수 있다.** 이 버전 이전에는 삭제 기록이 없었으므로, 데스크톱에만 남아 있던 행(모바일에서 지웠지만 데스크톱이 몰랐던 행)은 첫 대조에서 서버로 다시 올라간다. 데이터 유실보다 되살림을 택했다.
- **데스크톱 사용 횟수(`dictionary.usage_count`)는 자주 바뀌어 매번 올리지 않는다.** 단어를 추가·수정할 때 함께 올라간다.
여기 있는 항목은 고쳐야 할 갭이 아니라 **대안을 검토한 뒤 의도적으로 유지하기로 한 절충**이다.
§1 에 갭으로 재등록하지 마라.