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

@ -96,7 +96,8 @@ The existing `input-telemetry-handlers` and `suggestion-handlers` IPC extensions
|---|---|
| `ConfigService` | electron-store `AppConfig` (`configGet/Set`, defaults) |
| `LicenseService` | Freemium tiers, quotas (`daily_usage`), activation, upgrade prompts |
| `CloudSyncService` | Supabase sync, per-user DB switching, history/dictionary/meeting mirror |
| `CloudSyncService` | Supabase auth + lifecycle: per-user DB switching, local-mode import on first sign-in, Realtime (`ws` transport) + 5-min heartbeat, device check-in, debounced flush/pull triggers. The sync itself lives in `services/sync/` |
| `services/sync/` | `SyncEngine` (backfill once per user DB → push outbox → pull by server-clock keyset cursor → tombstones → memo-tag reconcile), `sync-outbox` (`sync_outbox`/`sync_state` tables), `sync-adapters` (history, dictionary, meetings, meeting memos/documents, custom commands, templates), `memo-tag-sync`, `supabase-sync-remote`, `device-registration`, `realtime-transport`. Services record changes with `getCloudSyncService().pushOne/pushDelete(entity, id)`; pulled rows are applied without re-queuing |
| `CloudSTTService` | Thin cloud STT wrapper over `D3ROCloudDriver` |
| `UpdateService` | electron-updater (canonical Forgejo feed, channels, mandatory/full-vs-delta policy, staged rollout, restart dialog) |
| `AutoLaunchService` | OS login-item auto-start |
@ -124,12 +125,12 @@ Registry: `src/main/ipc/index.ts` calls 31 `registerXHandlers()` in fixed order.
| `audio-handlers` | AUDIO |
| `caption-handlers` | CAPTION + SYSTEM_AUDIO |
| `chain-handlers` | CHAIN |
| `cloud-sync-handlers` | CLOUD_SYNC |
| `cloud-sync-handlers` | CLOUD_SYNC; forwards engine `data-changed` as `app:dataChanged` `{ type: 'cloud-sync', entities }` so History/Dashboard/Dictionary/Commands/Meetings/Templates reload |
| `config-handlers` | CONFIG |
| `context-handlers` | CONTEXT |
| `dictionary-handlers` | DICTIONARY |
| `file-transcription-handlers` | FILE_TRANSCRIPTION |
| `history-handlers` | HISTORY + `stats:getSummary` |
| `history-handlers` | HISTORY (incl. `history:setFavorite`) + `stats:getSummary` |
| `input-telemetry-handlers` | INPUT_TELEMETRY |
| `instruction-handlers` | INSTRUCTION |
| `keybinding-handlers` | KEYBINDING |

View file

@ -39,7 +39,7 @@ Provider stack: `GestureHandlerRootView → SafeAreaProvider → AuthProvider
| `ActionsScreen` | Voice-action parse/confirm/execute + history |
| `DictionaryScreen` | Dictionary CRUD/search/filters |
| `TeamsScreen` / `TeamDetailScreen` | Teams: create, members, invites, rename, leave, meetings, activity feed/comments |
| `DevicesScreen` | Registered devices, revoke/remove |
| `DevicesScreen` | Registered devices (phones, web, **desktop apps**), platform label, revoke/remove — revoking a desktop signs it out |
| `NotificationsScreen` | Notification list, permission, deep-link nav |
| `AccountScreen` | Profile, identities, data export/delete, logout |
| `DataPortabilityScreen` | Export/import archive, dictionary, history, meeting docs |

View file

@ -19,6 +19,7 @@
| Core schema + RLS + auth triggers | initial tables, profiles, triggers |
| Storage buckets | audio, meeting documents, exports |
| Teams | team invites, membership, roles, `team_activities` feed (`20260913000033`) |
| Cross-device sync | `20260927000034`: `sync_tombstones` (AFTER DELETE triggers on 8 synced tables, owner-read RLS, realtime, `prune_sync_tombstones_v1`), `history.revision` bump on content change, `meeting_memos.updated_at`, `(user_id, updated_at, id)` cursor indexes, client-keyed `sync_upsert/delete_user_template_v1`, realtime for `devices`/`memo_tags`/`custom_instructions`/`user_templates`, and `export_account_portability` projected back onto v1 archive keys |
| Knowledge / RAG | `knowledge_documents`, `knowledge_chunks`, pgvector |
| Notifications / push | push tokens, durable outbox |
| Billing | Payple, subscriptions, payment provider events/operations (`'stripe'` provider values kept only for historical rows) |
@ -78,7 +79,8 @@ Canonical product data lives in Supabase Postgres with RLS:
- **Content:** `history`, `meetings`, `meeting_memos`, `meeting_documents`, `memos`, `dictionary`, `commands`/instructions, `templates`.
- **Knowledge:** `knowledge_documents`, `knowledge_chunks` (pgvector).
- **Teams:** teams, members, invites, activity feed (`team_activities`, RPC-only writes, realtime-enabled).
- **Delivery:** `devices`, `push_tokens`, push outbox.
- **Delivery:** `devices` (mobile + desktop `windows`/`macos`), `push_tokens`, push outbox.
- **Sync:** `sync_tombstones` — deletion log the desktop mirror (and any offline client) reads by cursor.
- **Monetization:** `subscriptions`, `payment_provider_events/operations`, `ad_reward_claims`, IAP receipts, generation receipts.
- **Ops:** `audit_log` + admin operation records.
- **Portability:** `portable_exports` (+ storage).

View file

@ -41,12 +41,12 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]`
| AI-02 | Cloud LLM (Claude/OpenAI) | [x] | [x] | [x] | [x] | Desktop `PremiumLLMService`; web/mobile via `llm-proxy`; .NET `LlmProxyService` |
| AI-03 | Auto Polish (cleanup/filler removal) | [x] | [~] | [~] | [x] | Desktop built-in; web/mobile via commands. Desktop Auto Polish is the plain `refine` action (`llm-prompts.ts:14`), not a custom instruction, so it was **not** affected by the 2026-09-21 instruction-prompt fix (AI-05); regression cases now pin `refine`/`summarize`/`grammar`/`expand` (`VoiceModeService.test.ts:435`, `llm-prompts.test.ts:153`) |
| AI-04 | Translate / summarize / rephrase | [~] | [x] | [x] | [x] | Built-in instructions. **Desktop has two paths and only one of them worked.** The plain-action path (Settings → `defaultLLMAction`, `SettingsModal.tsx:653`) reads `BASE_SYSTEM_PROMPTS` directly and was always correct. The built-in *instruction* presets (`CustomInstructionService.ts:26/35/44/53/62`) ran through the custom-instruction path and inserted the instruction's own wording instead of the result — see AI-05. Fixed in `9c2b4d4` (2026-09-21), **not verified in a running app** (`11` GAP-LLM-02). Translate still always targets English: `AppConfig` has no target-language key and neither `language` (UI locale) nor `sttLanguage` (source language) can stand in (`llm-prompts.ts:37-52`, `11` GAP-LLM-01) |
| AI-05 | Custom instructions (user commands) | [~] | [x] | [x] | [x] | Web `commands`; mobile `CommandsScreen` — both go through Edge Functions and are unaffected. **Desktop: the custom-instruction path never worked in any shipped release.** The instruction was passed in the `text` argument of `processText(text, action, targetLanguage, customPrompt)` with the system-prompt argument left empty; `BASE_SYSTEM_PROMPTS` has no `custom` key, so resolution fell back to `refine` silently and the model polished the instruction it was handed — the transcript never reached it. Separately, only `{{text}}` was substituted and **none of the five built-ins use it** (`{{targetLanguage}}`, `{{userPrompt}}`, or no placeholder), so the substitution was a no-op from the day it was written. Introduced `fea923d` (2026-04-05); present `v0.1.0-alpha`..`v1.4.0`; **not a regression**. Three entry points were affected: commands-page activation (`CommandsPage.tsx:102-117`), command-popup selection (`bootstrap.ts:377-381`), voice keyword match (`VoiceModeService.ts:681-686`). Fixed in `9c2b4d4` (2026-09-21) — `llm-prompts.ts` is now the SSOT for placeholder substitution and argument placement (`renderInstructionPrompt:78`, `buildInstructionInvocation:101`, `resolveSystemPrompt:118`). The earlier "Red Team RT-03 verified" claim did not catch this and its cited evidence file (`red_team_log.md`) is not in the repo. `[~]` because unit tests pass (`llm-prompts.test.ts` 21, `llm-handlers.test.ts` 7, `VoiceModeService.test.ts` 22, `ChainService.test.ts` 5) but there is **no running-app verification** and the related `tests/red/{instruction,chain,voice,config}.usecase.test.ts` could not execute (`11` GAP-INFRA-06). See `11` GAP-LLM-01/02 |
| AI-05 | Custom instructions (user commands) | [~] | [x] | [x] | [x] | Web `commands`; mobile `CommandsScreen` — both go through Edge Functions and are unaffected. **Desktop: the custom-instruction path never worked in any shipped release.** The instruction was passed in the `text` argument of `processText(text, action, targetLanguage, customPrompt)` with the system-prompt argument left empty; `BASE_SYSTEM_PROMPTS` has no `custom` key, so resolution fell back to `refine` silently and the model polished the instruction it was handed — the transcript never reached it. Separately, only `{{text}}` was substituted and **none of the five built-ins use it** (`{{targetLanguage}}`, `{{userPrompt}}`, or no placeholder), so the substitution was a no-op from the day it was written. Introduced `fea923d` (2026-04-05); present `v0.1.0-alpha`..`v1.4.0`; **not a regression**. Three entry points were affected: commands-page activation (`CommandsPage.tsx:102-117`), command-popup selection (`bootstrap.ts:377-381`), voice keyword match (`VoiceModeService.ts:681-686`). Fixed in `9c2b4d4` (2026-09-21) — `llm-prompts.ts` is now the SSOT for placeholder substitution and argument placement (`renderInstructionPrompt:78`, `buildInstructionInvocation:101`, `resolveSystemPrompt:118`). The earlier "Red Team RT-03 verified" claim did not catch this and its cited evidence file (`red_team_log.md`) is not in the repo. `[~]` because unit tests pass (`llm-prompts.test.ts` 21, `llm-handlers.test.ts` 7, `VoiceModeService.test.ts` 22, `ChainService.test.ts` 5) but there is **no running-app verification** and the related `tests/red/{instruction,chain,voice,config}.usecase.test.ts` could not execute (`11` GAP-INFRA-06). See `11` GAP-LLM-01/02 **Sync (2026-09-27):** desktop user commands (not the desktop presets) mirror Supabase `custom_instructions` both ways, deletions included; server presets (`builtin_key`) are not copied to desktop. |
| AI-06 | Voice keyword commands | [~] | [-] | [ ] | [-] | Desktop `VoiceCommandService` + command popup. Keyword matching itself works (`VoiceModeService.ts:681-686`), but execution went through the broken custom-instruction path (AI-05), and a **second, separate defect** made the shortcut skip LLM processing entirely whenever `defaultLLMAction === 'none'` — which is exactly the value the commands UI and the command popup write when no command is active (`CommandsPage.tsx:116`, `bootstrap.ts:385`), so an explicitly named instruction was nullified by the default setting. Both fixed in `9c2b4d4` (`VoiceModeService.ts:779`/`:785`); **not verified in a running app** (`11` GAP-LLM-02) |
| AI-07 | LLM Chains (multi-step pipelines) | [~] | [ ] | [ ] | [-] | Desktop `ChainService`. The argument placement was already correct here, but chain steps **never substituted placeholders**, so `{{targetLanguage}}` / `{{userPrompt}}` reached the model verbatim as the system prompt. Now shares the same resolution function as the other two paths (`ChainService.ts:196`). Fixed in `9c2b4d4` (2026-09-21); 5 unit tests GREEN, but `tests/red/chain.usecase.test.ts` could not execute (`11` GAP-INFRA-06) and there is **no running-app verification** (`11` GAP-LLM-02) |
| AI-08 | Screen/context capture for prompts | [x] | [-] | [ ] | [-] | Desktop `ScreenContextService` |
| AI-09 | Streaming responses | [x] | [x] | [x] | [x] | SSE/NDJSON streaming |
| AI-10 | Dictation templates (voice form fill) | [x] | [ ] | [x] | [~] | Desktop `DictationTemplateService`; mobile `TemplatesScreen` |
| AI-10 | Dictation templates (voice form fill) | [x] | [ ] | [x] | [~] | Desktop `DictationTemplateService`; mobile `TemplatesScreen` **Sync (2026-09-27):** desktop custom dictation and meeting-document templates keep their id on the server through `sync_upsert_user_template_v1`/`sync_delete_user_template_v1`, which reuse the revision-checked mobile RPCs; builtins exist on both sides and are not copied. |
---
@ -56,17 +56,17 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]`
|---|---|---|---|---|---|---|
| MEM-01 | History list + search | [x] | [x] | [x] | [x] | Desktop SQLite; web/mobile Supabase |
| MEM-02 | History detail + edit | [x] | [x] | [x] | [x] | Mobile `HistoryDetailScreen` |
| MEM-03 | History favorites | [x] | [x] | [x] | [x] | |
| MEM-03 | History favorites | [~] | [x] | [x] | [x] | Desktop had **no** favorites until 2026-09-27 (the old `[x]` was wrong): `history.is_favorite` column, `HistoryService.setFavorite` + `history:setFavorite` IPC, star toggle in `HistoryEntryCard`; the value is the same Supabase `history.is_favorite` mobile filters on, so it syncs both ways (`apps/desktop/tests/main/sync/SyncEngine.test.ts` (20) + `apps/desktop/tests/integration/cross-device-sync.supabase.test.ts` (5, local Supabase stack)). `[~]` until a GUI run toggles it and mobile shows it |
| MEM-04 | History export / share | [x] | [~] | [x] | [x] | Desktop export; web limited; mobile share sheet |
| MEM-05 | History audio playback | [x] | [x] | [x] | [x] | Signed URLs on web/mobile |
| MEM-06 | Dictionary (custom vocabulary) | [x] | [x] | [x] | [x] | All surfaces CRUD; Desktop Red Team RT-02 & RT-18 fuzzed/verified |
| MEM-07 | Dictionary import/export | [x] | [x] | [x] | [-] | Desktop `dictionary:import/export` JSON+CSV (file dialogs, `DictionaryService`); web `serializeDictionary`/`importDictionaryFile` + header buttons; mobile CSV/TXT export + CSV/JSON/TXT import via `data-portability` |
| MEM-08 | Memos (tags over history) | [x] | [ ] | [x] | [x] | Desktop `MemoService`; mobile `MemosScreen`; web none |
| MEM-08 | Memos (tags over history) | [x] | [ ] | [x] | [x] | Desktop `MemoService`; mobile `MemosScreen`; web none. **Desktop tags sync with mobile since 2026-09-27**: pushed through the mobile RPCs (`mobile_add/remove_memo_tag_v1`) and reconciled by natural key `historyId` + normalized tag because the server owns tag ids and renames rows (`services/sync/memo-tag-sync.ts`). Tag normalisation (trim, collapse spaces, lower-case) is now identical on both sides |
| MEM-09 | Knowledge base / RAG add+index | [x] | [x] | [x] | [x] | Desktop local RAG (DEF-008 infinite chunking loop resolved, RT-08 verified); web/mobile cloud RAG |
| MEM-10 | Semantic search over knowledge | [x] | [x] | [x] | [x] | Web `KnowledgeSearch` calls `search-knowledge` (was mislabeled deferred); mobile + Edge `search-knowledge` |
| MEM-11 | Knowledge file upload | [x] | [x] | [x] | [x] | Web `.txt`/`.md` picker + newline-aware chunking + `embed-chunks`; desktop txt/md/pdf/docx; mobile file picker |
| MEM-12 | Voice actions (OS automation) | [x] | [x] | [x] | [x] | Desktop `VoiceActionService`; web `ActionRunner` (simulated); mobile `ActionsScreen` |
| MEM-13 | Cross-surface data sync | [~] | [x] | [x] | [x] | Desktop Supabase sync (V2-4); web/mobile native |
| MEM-13 | Cross-surface data sync | [~] | [x] | [x] | [x] | **Desktop rewritten 2026-09-27** (`services/sync/SyncEngine.ts`): persistent outbox (offline/restart safe), server-clock keyset cursors per user DB with pagination, pending local edits never overwritten by pull, deletions both ways through `sync_tombstones`, per-row failure isolation, Realtime that actually connects (`ws` transport). Covers history (+title/favorite), dictionary, meetings, meeting memos/documents, memo tags, custom commands, dictation + meeting-doc templates; plus device registration. Evidence: `apps/desktop/tests/main/sync/SyncEngine.test.ts` (20) + `apps/desktop/tests/integration/cross-device-sync.supabase.test.ts` (5, local Supabase stack). `[~]` until migration `20260927000034` is deployed to production and a desktop build is exercised against a phone (GAP-SYNC-05); knowledge/audio/settings still local (GAP-SYNC-06) |
| MEM-14 | Memo tag search | [x] | [ ] | [x] | [x] | |
---
@ -113,10 +113,10 @@ Status quick-reference: `[x]` done+verified · `[~]` partial/unverified · `[ ]`
| ACC-05 | Account profile / identity management | [~] | [~] | [x] | [x] | Desktop `LicenseTab`; mobile `AccountScreen` |
| ACC-06 | Logout + local sensitive purge | [x] | [x] | [x] | [x] | Mobile central purge GREEN |
| ACC-07 | Account deletion (server cascade + local purge) | [ ] | [ ] | [~] | [x] | `account-delete` Edge; mobile device E2E pending |
| ACC-08 | Cloud sync (per-user data) | [x] | [x] | [x] | [x] | Desktop `CloudSyncService`; SSOT D-* largely `[ ]` granular |
| ACC-09 | Device registration + revocation | [-] | [ ] | [x] | [x] | Mobile `DevicesScreen` |
| ACC-08 | Cloud sync (per-user data) | [~] | [x] | [x] | [x] | Desktop `CloudSyncService` + `services/sync/`. The earlier `[x]` hid shipped defects fixed on 2026-09-27 (GAP-SYNC-01/02): first pull after sign-in fetched nothing, only the first meeting's memos/documents were pushed, deletions never propagated, team meetings were pulled into the personal DB and their `team_id` cleared on re-push, Realtime never connected. Anonymous local-mode data is now imported into the first account that signs in (`db/index.ts importLocalModeData`). Production deploy pending (GAP-SYNC-05) |
| ACC-09 | Device registration + revocation | [~] | [ ] | [x] | [x] | Mobile `DevicesScreen`. **Desktop registers itself since 2026-09-27** (`services/sync/device-registration.ts`, platform `windows`/`macos`, installation id in `AppConfig.deviceInstallationId`); revoking it from the phone signs the desktop out (Realtime on `devices` + 5-min heartbeat), signing out on desktop unregisters it. Mobile parser accepted only `android`/`ios`, so one desktop row would have failed the whole Devices screen — fixed (`device-service.ts`, `__tests__/devices.test.ts`) and the card now names the platform. Local-stack evidence in the integration test; production pending GAP-SYNC-05 |
| ACC-10 | Offline queue + retry | [~] | [ ] | [x] | [x] | Mobile durable queue |
| ACC-11 | Data export/import (portability) | [~] | [ ] | [x] | [x] | Mobile canonical JSON E2E GREEN; desktop has export files |
| ACC-11 | Data export/import (portability) | [~] | [ ] | [x] | [x] | Mobile canonical JSON E2E GREEN; desktop has export files. **2026-09-27 backend fix:** `export_account_portability` serialised whole rows, so columns added after the v1 archive (meeting creation metadata, meeting-document template/idempotency) made any account with meetings fail the exact-key check in both the server restore and the mobile parser; the export now projects onto v1 keys (migration `20260927000034`, integration test asserts memo/meeting keys) |
| ACC-12 | Notification / push | [ ] | [~] | [~] | [x] | Backend transports for FCM + Web Push (VAPID) + APNs (.p8) + outbox cron drain implemented. Web/mobile client registration for webpush/apns still pending; Android delivery needs FCM project. |
---
@ -234,7 +234,7 @@ end-to-end behaviour has **not been verified by typing in a real app** (`11` GAP
| Surface | `[x]` | `[~]` | `[ ]` | Notable strength | Notable weakness |
|---|---|---|---|---|---|
| Desktop | ~40 | 3 | ~8 | Local AI pipeline, meetings, RAG, conversation, key bindings | Ads stubs, no team admin, no email account; custom-instruction/chain LLM path (AI-04..07) fixed 2026-09-21 but unverified in a running app |
| Desktop | ~40 | 6 | ~8 | Local AI pipeline, meetings, RAG, conversation, key bindings | Ads stubs, no team admin, no email account; custom-instruction/chain LLM path (AI-04..07) fixed 2026-09-21 but unverified in a running app; cross-device sync rewritten 2026-09-27, production migration deploy pending (GAP-SYNC-05) |
| Web | ~22 | 6 | ~14 | Server-shared data UX, billing, meetings, teams | No local AI, limited knowledge upload/search |
| Mobile | ~40 | 12 | ~18 | Cloud + native recording, portability, admin, IAP/ads | External store/console gates, a11y, deep E2E pending |
| Backend | ~45 | 6 | ~4 | RLS, Edge functions, billing, fail-closed AI | Payple webhook signature, some external provider keys |

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 에 갭으로 재등록하지 마라.