Skip to content

feat: peer-to-peer session sharing via Syncthing#45

Open
JayantDevkar wants to merge 289 commits intomainfrom
worktree-syncthing-sync-design
Open

feat: peer-to-peer session sharing via Syncthing#45
JayantDevkar wants to merge 289 commits intomainfrom
worktree-syncthing-sync-design

Conversation

@JayantDevkar
Copy link
Owner

@JayantDevkar JayantDevkar commented Mar 4, 2026

What this adds

Share Claude Code sessions with your team — automatically, securely, with zero cloud infrastructure.

When you use Claude Code, your sessions live in ~/.claude/ on your machine. That's great for solo work, but the moment you have two machines or a teammate, everyone's sessions are invisible to each other. You lose context, duplicate work, and can't learn from how others use Claude.

This PR adds peer-to-peer session syncing powered by Syncthing. Sessions travel directly between machines over encrypted connections. No servers, no accounts, no third parties touching your data.

How it works

You use Claude Code → session saved locally → watcher packages it
    → Syncthing sends it to teammates → appears on their dashboard
  1. Go to /sync — the setup wizard detects Syncthing and walks you through initialization
  2. Create a team — give it a name, you become the leader
  3. Invite teammates — they generate a join code, you paste it to add them
  4. Share projects — pick which projects the team should sync
  5. Accept subscriptions — each member controls what they send and receive
  6. Sessions flow automatically — within seconds on LAN, minutes over the internet

Four core concepts

Concept What it is
Member A person + machine combo (e.g., jayant.macbook). Same person on two machines = two members.
Team A group of members who can see each other's sessions. Teams are just access control — they don't store data.
Project A git repository shared with a team, identified by its git remote (e.g., org/repo). You choose what to share.
Subscription How you receive a shared project. Accept, pause, decline, or change direction (send-only, receive-only, both).

What you can do

  • Create and manage teams from the /team page — add members via join codes, share projects, view activity
  • Fine-grained subscriptions — accept, pause, resume, or decline any project. Choose sync direction per project.
  • See teammate sessions — browse their conversations, tool usage, and token costs on your dashboard
  • Multi-team support — be in different teams with different people, sharing different projects
  • Automatic reconciliation — a background timer keeps everything in sync (member discovery, device pairing, folder management)
  • Secure by default — TLS 1.3, mutual certificate auth, only paired devices can connect

What gets synced (and what doesn't)

Synced: session conversations, tool usage, token stats, subagent activity, session metadata

Never synced: your source code, secrets, .env files, anything outside ~/.claude/projects/, anything from projects you haven't shared


Architecture

The sync system is built in clean layers:

Domain Models (Team, Member, SharedProject, Subscription, SyncEvent)
    ↓
Repositories (SQLite persistence, UPSERT, FK cascades)
    ↓
Services (TeamService, ProjectService, ReconciliationService, MetadataService)
    ↓
Syncthing Abstraction (SyncthingClient → DeviceManager, FolderManager)
    ↓
FastAPI Routers (5 thin routers, dependency injection)
    ↓
SvelteKit Frontend (team pages, subscription management, setup wizard)

Domain-driven design: frozen Pydantic models with explicit state machines. Teams are active or dissolved. Members are added, active, or removed. Subscriptions are offered, accepted, paused, or declined. Invalid transitions raise errors.

3-phase reconciliation: runs every 60 seconds in the background

  1. Metadata — read peer state files, detect removals, discover new members and projects
  2. Mesh pair — ensure all active team members are paired in Syncthing
  3. Device lists — compute who should receive each project folder, apply declaratively

What's in the PR

Backend — api/

Layer Files What it does
Domain models api/domain/ (5 files) Team, Member, SharedProject, Subscription, SyncEvent — frozen Pydantic models with state machines
Schema api/db/schema.py v19 migration — 6 tables with FKs, cascades, CHECK constraints
Repositories api/repositories/ (5 files) SQLite CRUD with UPSERT, parameterized queries
Syncthing layer api/services/syncthing/ (3 files) Async HTTP client, DeviceManager, FolderManager
Sync services api/services/sync/ (5 files) Pairing, metadata, team lifecycle, project sharing, reconciliation
Routers api/routers/sync_*.py (5 files) Teams, projects, subscriptions, pending devices/folders, system
WatcherManager api/services/watcher_manager.py Background timer driving reconciliation + session packaging

Frontend — frontend/

Area What changed
/sync page Setup wizard with Syncthing detection and initialization
/team page Team listing with status badges and member counts
/team/[name] 5-tab detail: Overview, Members, Projects (with subscription management), Activity, Settings
Types api-types.ts updated with SyncTeam, SyncTeamMember, SyncSubscription, SyncEvent
Components TeamCard, TeamMemberCard, TeamProjectsTab (accept/pause/decline + direction picker), TeamActivityFeed

CLI — cli/karma/

  • Session packager, file watcher, Syncthing client wrapper
  • Config management (sync-config.json)
  • Worktree discovery for Claude Desktop sessions

Documentation — docs/about/


Numbers

  • 286 commits across the full development arc (v1 → v2 → v3 → v4 domain rewrite)
  • 298 files changed+65,615 / -2,037 lines
  • 295 sync v4 tests — domain models, repositories, services, E2E, router tests
  • 0 frontend build errors, 7 pre-existing warnings
  • 4-agent parallel code review caught 6 critical + 11 important issues, all fixed

Test plan

  • All sync v4 tests pass (295 tests)
  • Frontend builds cleanly (svelte-check — 0 errors)
  • Code review: domain, services, routers, security — all critical issues resolved
  • Documentation updated for v4 concepts
  • Manual test: full create → join → share → accept → sync flow between two machines
  • Manual test: subscription pause/resume/decline lifecycle
  • Manual test: team dissolve + cleanup verification

🤖 Generated with Claude Code

@JayantDevkar JayantDevkar changed the title feat: Syncthing session sync — pluggable backend for real-time team sharing feat: IPFS + Syncthing session sync — dual-backend team sharing Mar 4, 2026
@JayantDevkar JayantDevkar force-pushed the worktree-syncthing-sync-design branch 3 times, most recently from 47dbda4 to 9f85491 Compare March 7, 2026 06:34
@JayantDevkar JayantDevkar changed the title feat: IPFS + Syncthing session sync — dual-backend team sharing feat(sync): Syncthing-based team session sharing Mar 8, 2026
@JayantDevkar JayantDevkar changed the title feat(sync): Syncthing-based team session sharing feat: peer-to-peer session sharing via Syncthing Mar 18, 2026
JayantDevkar and others added 24 commits March 18, 2026 02:25
Confirms that Session.from_path() with remote JSONL paths correctly
resolves subagent directories via the standard session_dir layout.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add initial_prompt_images to SubagentSessionDetail schema and
  collectors so subagent overview pages show image attachments
- Remove isMainSession gate in ConversationOverview so both session
  and subagent views render images consistently
- Detect base64 image data in tool result content (ToolCallDetail)
  and render as inline images instead of raw text
- Add binary content placeholder for non-image base64 data

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Achieve full parity between local and remote sessions by packaging
missing resource types in the CLI and verifying all API path resolution
works end-to-end for synced remote sessions.

## CLI Packager (cli/karma/packager.py)
- Package file-history directories (~/.claude/file-history/{uuid}/) into
  staging for sync
- Package debug log files (~/.claude/debug/{uuid}.txt) into staging

## API Bug Fix (api/services/session_lookup.py)
- Fix early `return None` in find_session_with_project() when
  projects_dir doesn't exist — was blocking the remote session fallback
  path entirely. Now skips local scan gracefully and falls through to
  remote lookup.

## API Indexer (api/db/indexer.py)
- Add claude_base_dir parameter to _index_session() so remote sessions
  resolve resource paths (todos, tasks, file-history, debug) correctly
  during indexing instead of defaulting to ~/.claude

## Tests (72 new/modified tests, all passing)

### CLI Tests (cli/tests/test_packager.py) — 6 new
- TestFileHistorySyncing: copies file-history, skips missing, idempotent
- TestDebugLogSyncing: copies debug logs, skips missing, worktree support

### API Unit Tests (api/tests/test_remote_sessions.py) — 6 new
- TestRemoteSessionTodos: todos resolve via claude_base_dir
- TestRemoteSessionTasks: tasks resolve via claude_base_dir
- TestRemoteSessionToolResults: tool-results resolve via session_dir
- TestRemoteSessionFileHistory: file-history dir resolves
- TestRemoteSessionDebugLog: debug logs resolve and are readable
- TestRemoteSessionMissingResources: graceful empty returns

### API Integration Tests (api/tests/api/test_remote_sessions.py) — 8 new
- Endpoint tests for GET /sessions/{uuid}, /todos, /tasks, /subagents,
  /timeline, /file-activity, /tools, and 404 handling via remote fallback

### E2E Roundtrip Test (api/tests/test_remote_roundtrip.py) — 3 new
- Full pipeline: find_remote_session → verify all resources →
  index_remote_sessions → verify SQLite row

## Resource Parity Table (after this commit)
| Resource     | Packaged | API resolves | Verified |
|--------------|----------|--------------|----------|
| JSONL        | YES      | YES          | YES      |
| Subagents    | YES      | YES          | YES      |
| Tool-results | YES      | YES          | YES      |
| Todos        | YES      | YES          | YES      |
| Tasks        | YES      | YES          | YES      |
| File-history | YES (new)| YES          | YES (new)|
| Debug logs   | YES (new)| YES          | YES (new)|

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…-history

Code review of commit 8702c98 identified two simplification opportunities
in the CLI packager's package() method:

1. Compute `_claude_base` (self.project_dir.parent.parent) once in __init__
   instead of repeating it 4 times across resource-copying blocks.

2. Deduplicate tasks and file-history copying — both used identical
   copytree-by-uuid logic. Replaced with a single loop over
   ("tasks", "file-history") resource names.

The 3 distinct copy strategies are now clearly separated:
- Todos: glob pattern ({uuid}-*.json) → copy2
- Tasks + file-history: directory by uuid → copytree (deduplicated)
- Debug logs: single file ({uuid}.txt) → copy2

All 72 tests pass (26 packager + 46 remote session).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sync_project() was not passing extra_dirs to SessionPackager, so
worktree sessions were silently excluded from IPFS syncs. The
Syncthing path (main.py) already handled this correctly.

Now both sync backends discover worktree dirs via
find_worktree_dirs() and flatten them into the same outbox,
matching the Syncthing behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tabbed /sync page with 4 tabs: Setup (install/init/pair), Devices
(full monitoring), Projects (toggle sync with progressive disclosure),
Activity (live event feed + bandwidth chart). Replaces Syncthing's
localhost UI entirely. MVP persona: solo user with multiple machines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Backend: Syncthing proxy service, expanded /sync router with detect,
devices, projects, activity, init, enable/disable endpoints.
Frontend: tabbed /sync page (Setup, Devices, Projects, Activity),
nav update, Chart.js bandwidth chart, progressive disclosure components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Full-stack implementation of the Sync page that replaces Syncthing's
localhost UI with an integrated dashboard inside Claude Karma.

Backend (API):
- SyncthingProxy service layer wrapping CLI's SyncthingClient with
  async support via run_in_executor
- 11 endpoints: /sync/status, /sync/teams, /sync/detect,
  /sync/devices (GET/POST/DELETE), /sync/projects (GET + enable/disable/sync-now),
  /sync/activity, /sync/init
- Input validation with regex allowlists for device IDs and project names
- 46 tests covering all endpoints, proxy layer, and edge cases

Frontend (SvelteKit/Svelte 5):
- Setup tab: install detection, backend selection, init flow with
  machine name input, device ID display + copy
- Devices tab: expandable device cards with connection status,
  transfer stats, encryption details
- Projects tab: project list joined with sync folders, toggle sync
  per project, Select All, Sync Now actions
- Activity tab: bandwidth sparkline chart (Chart.js) with live
  upload/download rates, event log with colored status dots
- 10s polling with AbortController guard, URL-synced tab state
- Sync nav link in Header (desktop + mobile), skeleton route

Design docs updated with implementation amendments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ProjectsTab: fetch /sync/teams instead of /sync/projects for correct
  synced-project matching by encoded_name (Syncthing folder IDs differ)
- SetupTab: destructure .devices from API response, filter out self device
  so "This Machine" doesn't duplicate under Paired Devices
- sync_status.py: /sync/teams returns rich project objects (name,
  encoded_name, path); /sync/activity includes bandwidth stats
- syncthing_proxy.py: add get_bandwidth() via /rest/system/connections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…tals

- Poll /sync/activity every 3s instead of one-shot fetch
- Accumulate upload_rate/download_rate into history arrays client-side
  (API returns point-in-time values, not time series)
- Display upload_total/download_total below the chart
- Add FolderCompletion and ClusterConfigReceived event formatters
- Merge new events incrementally on each poll (dedupe by event ID)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Pass timeout=1s to Syncthing /rest/events when since>0 so it returns
  immediately instead of blocking until new events arrive
- Catch transient errors on events/bandwidth calls gracefully (return
  empty) while still propagating SyncthingNotRunning as 503
- Reduce requests timeout from 10s to 5s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…stats

Activity tab:
- Resolve Syncthing folder IDs → readable names using getProjectNameFromEncoded
- Resolve device IDs → device names via /sync/devices lookup
- Show events newest-first (reverse chronological)
- Load lookup maps on mount for enriching event display

Devices tab:
- Include in_bytes_total, out_bytes_total, type, crypto from Syncthing
  connections API (previously missing — showed 0 B for all devices)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…esolution

Activity tab:
- Add "Synced Folders" section showing per-folder stats from /rest/db/status
  (e.g., "3.1 GB, 4,145 files, 100%") — the actual transfer volumes
- Reuse getProjectNameFromEncoded for folder name resolution (no more hacks)
- Bandwidth section clarified: shows current session protocol traffic

Devices:
- Self device now shows aggregate transfer totals (not 0 B)
- Self device reports connected=true and is_self=true from API
- Remote devices include type/crypto from connections API

Proxy:
- get_folder_status() enriches folders with /rest/db/status (globalBytes,
  localBytes, needBytes, state, file counts)
- get_devices() fetches raw connections response for total + per-device stats
- Detects self device ID via /rest/system/status

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…e scale

Chart.js canvas cannot resolve CSS custom properties. Resolve --text-muted
via getComputedStyle for tick labels. Add suggestedMax: 1024 to prevent
degenerate 0-1 Y-axis when idle. Increase line visibility (borderWidth 2,
fill alpha 0.15) and limit Y ticks to 4.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Connection-level inBytesTotal/outBytesTotal only show protocol overhead
since last Syncthing restart (~4KB), not actual sync data. Replace with
folder-level inSyncBytes aggregated by type: sendonly folders → "Synced out",
receiveonly folders → "Synced in". Now shows real volumes (3.1 GB / 17.6 MB).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add rescan_folder() and rescan_all() to SyncthingProxy (POST /rest/db/scan)
- Wire /sync/rescan endpoint for global rescan, /sync/projects/{name}/sync-now
  for per-project rescan with folder ID matching
- Add "Sync Now" button in Activity tab header (rescans all folders)
- Show "Sync Now" button on all synced projects in ProjectRow (not just pending)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Project display name "claude-karma" didn't substring-match Syncthing
folder ID "karma-out-jay-claude-code-karma". Fix by:
- Frontend sends encoded_name instead of display name for better matching
- API matches against folder id, path, and label
- Increase project name length limit to 512 for encoded paths

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_load_config() silently returned None when sync-config.json had trailing
duplicate data (json.JSONDecodeError). This caused /sync/teams to return
empty, making all projects show as "not synced" in the Projects tab.

Add partial JSON recovery via json.JSONDecoder.raw_decode() as fallback
when standard json.loads() fails.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… stats

Backend (API):
- Fix SyncthingProxy singleton never reconnecting after Syncthing starts late
  (_require_client retries _try_connect when client is None)
- Fix detect() returning running:true when status API call fails
- Consolidate dual config loaders (_load_config + _load_sync_config) into
  single _load_sync_config using CLI's SyncConfig.load() with error recovery
- Centralize sys.path manipulation (was repeated 3 times, now once at module level)

Frontend — shared types & utilities:
- Add SyncDetect, SyncStatusResponse, SyncDevice, SyncProject to api-types.ts
  (removes 7 duplicate interface definitions across 4 files)
- Add formatBytes() and formatBytesRate() to utils.ts with consistent
  1024-based units (fixes inconsistency: ActivityTab used 1000-based,
  DeviceCard used 1024-based)

Frontend — component fixes:
- SetupTab: replace State 3 device list (~200 lines) with overview dashboard
  showing 4 stat cards (Devices, Synced Projects, Synced In, Synced Out)
  plus machine details. Eliminates full duplication of DevicesTab.
- DevicesTab: now owns all device management (pair + remove with confirm),
  previously split between SetupTab and DevicesTab
- ProjectsTab/ProjectRow: fix name vs encoded_name bug — handleToggle and
  selectAll were sending display name instead of encoded_name to API
- ProjectRow: remove non-functional placeholder buttons (Files/Sync History)
- ActivityTab/BandwidthChart/DeviceCard: replace local formatBytes and
  formatRelativeTime with shared utils

Net: -149 lines, 1 critical bug fixed, 2 data bugs fixed, zero type errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- All tab components reload data on tab switch via active prop + $effect
- DevicesTab shows flash messages on add/remove, auto-uppercases device ID
- DeviceCard uses amber dot for disconnected, adds copy device ID button
- ProjectsTab uses live Syncthing device count instead of stale team config
- SetupTab conditionally renders version to avoid blank space
- Duplicate device add returns HTTP 409 with SyncthingClient pre-check
- Improved confirmation popover styling for device removal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add find_desktop_worktree_dirs() and find_all_worktree_dirs() to discover
Desktop worktrees (~/.claude-worktrees/) alongside CLI worktrees. Wire
into sync, watch, and status commands so all worktree sessions are
packaged for Syncthing sync.

- find_desktop_worktree_dirs: scans for -claude-worktrees-{project}- pattern
- find_all_worktree_dirs: combines CLI + Desktop discovery with dedup
- project_name_from_path: extracts dir name from full project path
- Watch command now monitors Desktop worktree dirs for changes
- Status command counts Desktop worktree sessions
- Diagnostic tests verify discovery works on real filesystem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…iews

Team Management:
- New TeamSelector component: team switching dropdown, inline stats,
  "+ New Team" entry point between tab strip and content
- Active team context passed to Overview/Members/Projects tabs
- Team create/delete in OverviewTab with confirmation dialog
- Auto-select first team, react to team changes across all tabs

Projects Tab UX:
- Search/filter input for project list (type-to-filter, case-insensitive)
- Sort: synced projects first, then alphabetical
- Replace 12px green dot toggle with 16x16 checkbox (WCAG 2.5.8)
- Confirmation dialog when disabling sync (prevents accidental toggles)
- "Select All" renamed to "Enable All (N)" with inline confirmation
- Disambiguate duplicate project names with path subtitle
- Remove lossy decodePath — pass encoded_name directly to API
- Flash messages for all sync actions via onaction callback

Overview Tab UX:
- Human-readable pending folder names (parse karma-out-{user}-{project})
- Descriptive sync engine banner explaining what Start does
- Remove duplicate team creation form (single entry via TeamSelector)
- Unify ID terminology: "Your Name", "Machine", "Sync ID"
- Watch start sends { team: teamName } in POST body

Members Tab UX:
- Accept teamName prop instead of auto-selecting first team
- Unify terminology: "Device ID" → "Sync ID" everywhere
- Clearer helper text for onboarding ("Paste their Sync ID here")
- Reload data when team switches

Data Consistency:
- TeamSelector defensive counting (member_count ?? members.length)
  fixes "0 members · 0 projects" display bug
- Jargon elimination: "Packaged sessions" → "Queued for sync",
  gap warning points to Overview tab's sync engine

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JayantDevkar and others added 25 commits March 18, 2026 02:29
Projects that only exist on teammates' machines (synced via Syncthing)
were returning 404s. Now the project detail endpoint checks for remote
sessions before giving up, the branches endpoint serves DB data for
non-local projects, and display_name is derived from git_identity
(e.g. "owner/repo" → "repo") in upsert_team_project and the indexer
so remote-only projects show readable names instead of raw encoded paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After deleting remote sessions, remote-only projects survived in the
projects table and title cache, causing them to keep appearing in the
project listing. Now removes orphan project rows (no remaining sessions)
and their corresponding title cache files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comprehensive design for sync feature rewrite with Pydantic domain
models, repository pattern, and simplified 3-phase reconciliation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes from code review:
- C1/C2: sync_projects PK changed from encoded_name to git_identity
  (universal cross-machine key), cascaded to subscriptions FK
- C4: Added typed SyncEvent model with event types and detail structures
- I1: Added session packaging integration with subscription gating
- I2: Clarified FolderManager subscription-to-folder mapping
- I3: Specified metadata folder structure (team.json, member states, removal signals)
- I4: Member.remove() now allows ADDED → REMOVED transition
- I5: Documented dissolve_team cascade behavior at domain level
- I6: Added cleanup logic for member removal, project removal, and auto-leave
- I7: Documented sync_removed_members purpose in v4
- S4: Added updated_at to sync_members
- S6: Added direction-to-Syncthing folder type mapping table
- S7: Added cross-team device unpair safety check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- N1: sync_events column renamed project_encoded_name → project_git_identity
- N3: record_removal() now accepts member_tag for audit
- N5: MetadataService.write_own_state() adds projects parameter
- N6: FolderManager adds remove_outbox_folder() and cleanup_project_folders()
- N7: Phase 1 reconciliation detects removed projects and declines subscriptions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Master plan with dependency graph, agent recommendations, and file map.
Phase 1: Foundation (domain models + schema v19 + repositories)
Phase 2: Infrastructure (Syncthing client + managers + pairing)
Phase 3: Services (team, project, metadata, reconciliation)
Phase 4: API (routers + cleanup + E2E test)

Phases 1 and 2 can run in parallel. 26 tasks total across 4 phases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Immutable Team model with TeamStatus enum, add/remove member operations
guarded by leader authorization, dissolve transition, AuthorizationError
and InvalidTransitionError exception types. 16 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Immutable Member model with MemberStatus enum (ADDED/ACTIVE/REMOVED),
member_tag computed property, from_member_tag classmethod, and activate/remove
state machine with InvalidTransitionError guards. 12 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Immutable SharedProject model with SharedProjectStatus enum, derive_folder_suffix()
function (strips .git, replaces / with -), folder_suffix computed property,
and remove() state transition. encoded_name is Optional[str]. 14 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Immutable Subscription model with SubscriptionStatus and SyncDirection enums,
full state machine (accept/pause/resume/decline) with InvalidTransitionError guards,
and change_direction() gated on ACCEPTED status. 22 tests passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
18-value SyncEventType enum + immutable SyncEvent model with optional
subject_id and payload. TDD: test file written first, all 18 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SyncthingClient: pure HTTP wrapper for Syncthing REST API
- DeviceManager: pair/unpair/connection status
- FolderManager: outbox/inbox/device list/cleanup operations
- PairingService: permanent pairing code encode/decode (base32)
- 90 tests passing across 4 test files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… be <4 chars)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Drops all v3 sync_* tables and recreates with v4 schema:
- sync_projects PK is (team_name, git_identity)
- sync_subscriptions references project_git_identity
- sync_events uses project_git_identity column
- sync_members has updated_at column
- All indexes created

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Domain models: fixed field names to match spec (name, leader_device_id,
  leader_member_tag, member_tag, git_identity, project_git_identity)
- Team.add_member/remove_member take Member objects (not device strings)
- SyncEvent.team_name is Optional (not required)
- All 5 repositories implemented with UPSERT save()
- 225 tests passing across domain models, repos, schema, Syncthing, pairing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TeamService: create_team, add_member, remove_member, dissolve_team
ProjectService: share_project, remove_project, accept/pause/resume/decline
  subscription, change_direction
39 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
3-phase reconciliation (metadata → mesh-pair → device-lists) that runs
every 60s. Detects removal signals, discovers new members, declines subs
for removed projects, pairs active peers, and applies declarative device
lists to Syncthing folders. 19 tests, all passing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests full journey: create team → share project → pairing code →
add member → accept subscription → change direction → remove member.
5 tests passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- sync_deps.py: shared dependency factories (conn, config, repos, services)
- sync_teams.py: team CRUD + member management via TeamService
- sync_projects.py: project sharing + subscription lifecycle via ProjectService
- sync_pairing.py: NEW — pairing code generation/validation + device status
- sync_system.py: simplified — status, init, detect, reset + new /reconcile
- main.py: register v4 routers, remove v3 router imports
- 44 router tests (all passing) with mocked services + real in-memory DB

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace 6-phase MetadataReconciliationTimer with ReconciliationTimer
  that calls ReconciliationService.run_cycle() (3-phase pipeline)
- Gate session packaging by v4 subscriptions (send|both direction)
- Log events via v4 EventRepository instead of v3 sync_queries
- Remove _ConfigProxy/_SyncthingProxy (no longer needed)
- Preserve H1 fix: dedicated SQLite connection per timer thread
- Preserve 120s timeout on async reconciliation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tecture

Delete 12 v3 service/router/db files:
- routers: sync_members, sync_pending, sync_devices, sync_operations
- services: sync_identity, sync_policy, sync_folders, sync_reconciliation,
  sync_metadata_reconciler, sync_metadata_writer, syncthing_proxy
- db: sync_queries

Delete 30 v3 test files (all tested deleted code).

Fix 2 production files with lazy imports of deleted modules:
- db/indexer.py: use v4 EventRepository + MemberRepository
- services/remote_sessions.py: use v4 MemberRepository

313 v4 tests passing. No new regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- C1: add _assert_active() to Team.add_member/remove_member
- C2: InvalidTransitionError now extends ValueError (per spec)
- C3: catch InvalidTransitionError → 409 in all 8 router endpoints
- C6: use SharedProjectStatus.SHARED enum instead of string comparison
- I1: add dot validation in Member.from_member_tag
- I5: move event log before team delete in dissolve_team
- I8: add _validate_path_component() for path traversal prevention
- Extend SyncthingClient._delete to accept params, simplify dismiss methods

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Backend (11 new endpoints):
- sync_pending.py: pending devices (list/accept/dismiss) + folders (list/accept/reject)
- sync_teams.py: join-code, activity, project-status, session-stats
- SyncthingClient: get_pending_devices, get_pending_folders, dismiss methods

Frontend (15 files):
- api-types.ts: v4 types (SyncTeam, SyncTeamMember, SyncSubscription, etc.)
- Server loaders: single-call team detail, simplified team listing
- Components: TeamOverviewTab, TeamMembersTab, TeamProjectsTab (subscription
  management with accept/pause/resume/decline + direction picker),
  TeamActivityTab, CreateTeamDialog, JoinTeamDialog, AddProjectDialog

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…l API

- syncing-sessions.md: add subscriptions concept (4th pillar), update
  team lifecycle (leader/dissolved), fix folder naming to member_tag +
  folder_suffix, update join flow with pairing codes, revise settings
- api-reference.md: add 30+ sync endpoints across 5 sections (system,
  teams, projects/subscriptions, pending, pairing), add 403/409 status
  codes, add example responses
- quick-start.md: replace CLI device-id flow with dashboard-first setup
  wizard and join code workflow
- features.md: replace device-id joining with join codes, add subscription
  control section, remove CLI command examples
- architecture.md: expand sync endpoint listing with v4 routes

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JayantDevkar JayantDevkar force-pushed the worktree-syncthing-sync-design branch from 8e66a04 to 55f00be Compare March 18, 2026 09:31
JayantDevkar and others added 4 commits March 18, 2026 02:45
In v4, only the leader adds members by pasting their pairing code.
There is no self-service "join team" action.

- Remove JoinTeamDialog from team listing page
- Remove JoinCodeCard from team overview tab
- Remove join-code fetch from team detail server loader
- Update member tab messaging to reflect v4 pairing flow

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Leaders can now add teammates from the Members tab by pasting
their pairing code. Includes inline form with error handling,
loading state, and link to /sync where codes are generated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…riptions

- Fix subscription field mismatch (API sent "project", frontend expected "project_git_identity")
- Fix sync/status teams format from dict to array to match frontend types
- Add prominent v4 pairing code card on /sync overview (replaces v3 join code)
- Redesign /team empty state with dual-path: leader (create team) vs member (share pairing code)
- Add GettingStartedBanner component for new team onboarding (3-step guide)
- Add OFFERED subscription notification banner on team Projects tab
- Replace v3 TeamSettings tab with clean Leave/Dissolve actions
- Add pending subscription badge to TeamCard on team list page
- Remove v3 dead code: JoinTeamDialog, JoinCodeCard, SessionLimitSelector, TeamSettings, join-code.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rter

base32-encoding a payload containing a Syncthing device ID (already base32)
produced 87-block codes (434 chars) — unusable for copy-paste sharing.

Switching to base64url with 6-char blocks reduces codes to ~23 blocks (157
chars) for typical member_tags. Backwards-compatible: validate_code()
auto-detects legacy base32 codes (all uppercase) vs new base64url codes.

Also adds TestMemberGeneratesLeaderDecodes test class with 7 end-to-end
tests covering the real v4 flow: member generates code → shares out-of-band
→ leader decodes on their machine.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants