Skip to content

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

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

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

Conversation

@JayantDevkar
Copy link
Copy Markdown
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:27
…mote badge to subagent sessions

Remove redundant remote_user_id and desktop session badges from the Model
card in ConversationOverview, since they were already displayed in the
ConversationHeader badges section. Clean up unused imports (Globe, Monitor,
getTeamMemberColor, isRemoteSession) from ConversationOverview.

Add remote device badge to the subagent session header badges snippet in
ConversationHeader, so subagent sessions from remote/synced devices now
show the team member name badge next to the header — matching the behavior
already present for main sessions.

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

Remove Strategy 2 (join-code trust fallback) from _auto_accept_pending_peers
which derived usernames from Syncthing hostnames (e.g. "jayants-mac-mini").
Devices are now only accepted when they offer karma-* folders, ensuring
usernames are always correct from the real karma user_id in the folder ID.

This fixes three interconnected bugs:
- Bug 1: Inbox folders deleted by name-change cleanup were never recreated
  because auto_only mode blocked the recreation loop
- Bug 2: Watcher silently consumed pending offers (handshake + own-outbox)
  leaving users confused about disappearing pending items
- Bug 3: No recovery mechanism existed for folders deleted during name
  correction since the watcher's auto_only gate prevented recreation

Also removes the stale folder deletion from _accept_pending_folders pre-scan
(no longer needed since wrong-name folders are never created), and improves
the pending request UX:
- Add description field to pending API response with human-readable context
  (e.g. "Receive sessions from jayant for claude-karma")
- Filter out handshake folders from frontend (infrastructure signals only)
- Rename section "Incoming Project Shares" → "Pending Session Shares"
- Show actionable helper text explaining what Accept does

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

- Add POST /sync/pending-devices/{id}/accept endpoint that pairs device in
  Syncthing, adds team member, creates handshake folder, and auto-shares
  project folders
- Add DELETE /sync/pending-devices/{id} endpoint to dismiss pending requests
  via Syncthing API
- Add ALLOWED_MEMBER_NAME regex allowing dots for hostnames (fixes silent
  400 errors on remove/add member for names like "Jayants-Mac-mini.local")
- Show "Pending Requests" section on team detail page with Accept/Dismiss
  buttons matching the existing "Pending Session Shares" pattern
- Add diagnostic hints when waiting for members (Syncthing status, commands)
- Filter own outbox and handshake folders from pending shares display
- Improve project labels using git identity DB lookup instead of broken
  folder ID parsing (shows "claude-code-karma" instead of raw suffix)
- Simplify teams list page pending banner to point users to team detail

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a team leader creates a joiner's outbox folder, they may use
the Syncthing hostname (machine_id) instead of the karma user_id.
The joiner's own-outbox detection only checked user_id, causing
their outbox to appear as "Receive sessions" instead of "Send
your sessions".

- API: check both user_id and machine_id via own_names set when
  classifying pending folders; add folder_type "outbox" for own
  outbox vs "sessions" for others
- CLI: check both own_user_id and config.machine_id prefixes in
  _accept_pending_folders so own outbox is created as sendonly
- Frontend: add "outbox" to folder_type union, include in pending
  filter, show distinct styling and "Send your sessions" copy

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remote sessions were invisible in the dashboard until the next periodic
reindex cycle (up to 5 minutes). Now trigger index_remote_sessions()
immediately after folder/device acceptance, member addition, and project
sharing — on both sender and acceptor sides.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ntent, inherit UX

Fix custom skills (e.g. "pdf") being misclassified as commands on importing
machines by removing the LIKE '%:%' filter from the packager and adding
dual-source classification overrides (manifest + JSONL path extraction).

Add skill_definitions table (schema v12) to cache extracted SKILL.md content
from remote session JSONL files. Extend skill/command usage queries with
remote/local split (remote_count, local_count, remote_user_ids, is_remote_only).

Add POST /skills/{name}/inherit and /commands/{name}/inherit endpoints with
path traversal validation to create local SKILL.md/command files from remote
definitions. Frontend shows "Remote" badge on remote-only skills/commands,
skill detail page shows content preview with source attribution, and new
InheritModal allows scope selection (user vs project).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add per-user colored segments to analytics charts so users can
visually distinguish local vs. team member data when remote
sessions exist via Syncthing sync.

Backend:
- Add _resolve_user_names() and _query_per_user_trend() helpers
- query_analytics() returns start_times_with_user and user_names
- _query_item_usage_trend() returns trend_by_user and user_names
- Add sessions_by_date_by_user/user_names to ProjectAnalytics schema
- Add trend_by_user/user_names to UsageTrendResponse schema

Frontend:
- Add hex color utilities (getUserChartColor, getUserChartLabel,
  getTeamMemberHexColor) with shared teamMemberPaletteIndex hash
- Velocity bar chart: stacked bars per user when multi-user
- SessionsChart: multi-line per user with colored lines and legend
- UsageAnalytics: "By Item / By User" segmented toggle
- Extract shared fillDateRange/getLocalDateKey/formatLocalDate
  helpers in SessionsChart to fix single-day edge case
- By-user mode bypasses itemDisplayFn/itemLinkFn for user names

All changes are additive — single-user charts render identically
to before. Local user shown as accent purple, remote users get
deterministic team member colors.

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

SQLite prohibits expressions in PRIMARY KEY constraints. Use
NOT NULL DEFAULT '__local__' on source_user_id instead, making
the composite PK (skill_name, source_user_id) work directly.

Fixed in all 3 locations: initial DDL, ensure_schema catch-up,
and v12 migration. Also updated indexer queries to use plain
column comparison instead of COALESCE wrapping.

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

Root cause: The SQLite fast path in the project detail endpoint returned
early without checking disk for unindexed remote sessions. When Syncthing
delivered files between periodic reindex cycles (every 300s), remote
sessions were invisible on the dashboard.

Fix 1 — SQLite fast path remote merge (projects.py):
After querying the DB, now also checks the filesystem for remote sessions
not yet indexed. Merges lightweight SessionSummary entries from disk
metadata and triggers a background reindex so the next request is served
entirely from the DB.

Fix 2 — RemoteSessionWatcher (watcher_manager.py + main.py):
New filesystem watcher on ~/.claude_karma/remote-sessions/ using watchdog
(same debounce pattern as the existing SessionWatcher). When Syncthing
delivers JSONL files, detects the change within 5s and triggers
trigger_remote_reindex(). Starts at API boot and in WatcherManager.

Together: Fix 1 gives instant visibility on first request, Fix 2 keeps
the DB consistent within seconds of file arrival.

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

Split CLI god module (1,255→760 lines):
- Extract cli/karma/folder_ids.py — shared folder ID parsing (CLI + API)
- Extract cli/karma/pending.py — pending folder acceptance with 3 handlers
- Extract cli/karma/project_resolution.py — project resolution logic

API improvements:
- Replace 13× bare `except: pass` with proper logger calls
- Thread-safe singleton access for proxy/watcher (threading.Lock)
- TTL cache on _load_identity() to avoid re-reading config every request
- Move 9 inline request models from sync_status.py to schemas.py
- Add VALID_SESSION_LIMITS validation in sync_queries.py
- Add _ALLOWED_EVENT_FILTERS column allowlist for query_events
- Fix missing `import json` in remote_sessions router
- Use settings.karma_base instead of hardcoded paths
- Deduplicate _get_local_user_id (router imports from service)
- Filter .stversions/.syncthing.* temp files in watcher

CLI fixes:
- Bump requires-python to >=3.10 (code uses str|None syntax)
- Add PermissionError/OSError handling in packager stat() and shutil ops
- Replace print() with logger.exception() in watcher

Frontend consolidation:
- Extract shared formatSyncEvent/syncEventColor/isSyncEventWarning utils
- TeamActivityFeed uses shared event formatting
- Add error states for syncAllNow, acceptFolder, rejectFolder actions

Tests: invalidate identity cache in fixtures to prevent TTL leaking.

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

- Consolidate category_from_base_directory into canonical helper (removes 2 duplicates)
- Fix plugin command misclassification (check /plugins/cache/ before /skills/ and /commands/)
- Fix cli_js path detection for Homebrew Cask installs (native binary, no cli.js)
- Fix skill_definitions extraction: widen lookahead window 3→8 for ProgressMessage gaps
- Fix ON CONFLICT to DO UPDATE (fill content from later sessions with actual content)
- Fix MIN(indexed_at) for manifest reclassification (was MAX, skipping stale sessions)
- Wire per-user trend data through skills and agents endpoints
- Add by-user toggle switch in UsageAnalytics chart component
- Move inherit UX to /skills/[skill_name] detail page, simplify [...path] to read-only banner
- Replace subagent inner loop with collect_agent_data() call in collectors.py

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>
…gation

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

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

Members grid with per-member color borders, connection status, data transfer
stats, 14-day sparkline activity charts, and remove confirmation flow.

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

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

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…embers/Projects/Activity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix critical bug: unwrap API response in TeamActivityTab period switcher (data.stats)
- Fix hex color consistency: use light-mode hex values for new palette entries
- Use LOCAL_USER_HEX constant instead of hardcoded #7c3aed in ProjectMemberBar

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

Bug fixes:
- Fix SSR crash: initialize $state directly from data props instead of $effect
  (which doesn't run during SSR), preventing "Team not found" on page load
- Fix 500 error in ProjectMemberBar: add null safety for received_counts/local_count
- Fix Chart.js "can't acquire context" error: move chart creation from onMount
  to $effect with canvas existence guard
- Fix empty activity chart: session_received events were logged without team_name
  in indexer.py, making them invisible to per-team queries
- Fix Members tab always showing "No transfer data": look up real Syncthing
  device stats from devices array matched by device_id

API improvements:
- Add comma-separated event_type filter support (e.g. session_packaged,session_received)
- Add member_name query parameter to activity endpoint for per-member filtering
- Resolve team_name from sync_team_projects before logging session_received events

UX improvements:
- Redesign TeamActivityFeed with card layout, type filter pills, and
  color-coded member filter pills
- Change Overview stat from "Projects in sync" to "Unsynced Sessions" count
- Center-align tab navigation
- Add empty state message for activity chart when no data exists

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and cache remote session scans

The /projects/{encoded_name} endpoint was significantly slower than /sessions/all due to three issues:

1. A single try/except wrapped the entire SQLite fast path (143 lines). Any error in
   chain info, remote session merge, or title enrichment would discard the successful
   DB result and trigger the catastrophic JSONL fallback (parses every session file
   before paginating).

2. list_remote_sessions_for_project() performed a full filesystem walk (~4s for 942
   remote sessions) on every request with no caching.

3. Chain info opened a redundant second DB connection.

Fixes:
- Isolate try/excepts: core DB query, chain info, and remote merge each have independent
  error handling. Only a DB query failure triggers JSONL fallback. Chain info degrades
  gracefully to empty dict.
- Add thread-safe TTLCache (cachetools, 30s, maxsize=128) with double-checked locking
  for remote session scans. Repeated requests go from ~4.1s to ~0.02ms.
- Reuse single sqlite_read() connection for both query_project_sessions and
  query_chain_info_for_project.
- JSONL fallback path also uses cached remote sessions.

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

Three interrelated bugs caused incorrect remote/local skill classification
and prevented the "Inherit Skill" feature from working for remote plugin skills.

Fixes:

1. is_remote_only logic flaw — Skills installed locally (e.g. superpowers:executing-plans)
   were incorrectly tagged as remote-only when all session usage came from remote users.
   Now checks local file/directory existence before marking remote-only, covering:
   - Plugin skills (directory check via is_plugin_installed_locally)
   - Bundled skills/commands (always local)
   - Custom skills (SKILL.md file check)
   - User commands (.md file check)
   Fixed in both listing (get_skill_usage) and detail (get_skill_detail) endpoints.

2. plugin field null for remote skills — When skill_info is None (plugin not
   installed locally), the plugin field was always null. Now derives plugin name
   from skill_name.split(":")[0] as fallback.

3. Plugin skill definitions never extracted — _extract_skill_definitions_from_session
   skipped all plugin_skill categories, so remote plugin skills never got their
   definitions saved to skill_definitions table. Added Pass 3 fallback for
   unclassified colon-containing skills and allowed remote plugin_skill definitions
   through the filter. This enables "Inherit Skill" for remote plugin skills.

Also added public API functions is_plugin_installed_locally() and
is_custom_skill_local() to command_helpers to avoid private imports.

Verified against all cases:
- Case 1: Both have, both use → is_remote_only=false, local content shown
- Case 2: Plugin only remote has → is_remote_only=true, Inherit Skill available
- Case 3: Both have, only remote used → is_remote_only=false (was the primary bug)
- Case 4a: Plugin only remote has → is_remote_only=true, Inherit Skill available
- Case 4b: Custom skill only remote has → is_remote_only=true, Inherit Skill available
- Bundled edge case → is_remote_only=false (always local)

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

New read-only member page with color-themed profile header and 4 tabs:
- Overview: stats grid, daily session chart, project contribution list
- Sessions: expandable project/session list from remote sessions API
- Teams: team cards with project contribution badges
- Activity: type-filtered cross-team activity feed with load-more

Backend adds /sync/members/{member_name} aggregated profile endpoint and
/sync/members/{member_name}/activity for cross-team event pagination.
TeamMembersTab cards now link to the member detail page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JayantDevkar and others added 30 commits March 20, 2026 05:49
CommandsPanel in session detail view was opening a modal that fetched
/skills/info/ which failed. Now uses <a href="/commands/{name}"> links
to navigate to the existing command detail page.

Removed all dead modal code (modal state, fetch logic, markdown
rendering, copy button, stripFrontmatter helper, unused imports).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds git-radio as a submodule for coordinating cross-machine testing
of the sync v4 lifecycle. The scenario covers 34 steps across 11
phases: prerequisites → team creation → project sharing → member
addition → Syncthing handshake → reconciliation → subscription →
session packaging → direction change → cleanup → audit trail.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FastAPI runs sync dependency functions (get_read_conn, get_conn) in a
threadpool, but async endpoint handlers run on the event loop thread.
This cross-thread usage triggers SQLite's thread safety check. Safe to
disable since read connections are per-request and the writer is
lock-protected.

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

Bug 1: accept_pending_folder() used wrong path for metadata folders
(karma_base/folder_id instead of karma_base/metadata-folders/folder_id)
and wrong type (receiveonly instead of sendreceive). Added centralized
resolve_folder_path() and resolve_folder_type() in folder_manager.py.
All call sites now use the single source of truth.

Bug 2: dissolve_team() hard-deleted the team row, making it impossible
to query the team or its activity log after dissolution. Changed to
soft-delete (UPDATE status='dissolved'). Added list_active() to
TeamRepository. list_teams endpoint now filters active-only by default
with ?include_dissolved=true option.

Found during cross-machine walkie test (walkie/sync-test branch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents auto-leave when creating a team with the same name as a
previously dissolved team. Stale removed/*.json files from the prior
incarnation are deleted during create_team().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compares removal_at timestamp against team.created_at. If the removal
signal is older than the current team creation, it's from a prior
incarnation and is ignored. Defense-in-depth against Syncthing
re-syncing old data from peers who haven't dissolved yet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Closes the Accept & Pair timing race: when a device is accepted but
the metadata folder hasn't propagated to pending yet, the next
reconciliation cycle (60s or manual POST /sync/reconcile) will now
pick it up and auto-accept it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of a single reconcile call, poll up to 4 times with 3s
intervals. Each attempt triggers reconciliation (which now also
scans pending folders) then checks if the team record exists.
Closes the UX gap where Accept & Pair appeared to do nothing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents false-positive stale detection when removal signals are
written milliseconds before team.created_at (common in tests and
rapid dissolve+recreate scenarios).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes found during walkie/sub-test 39-step cross-machine scenario
(Sync v4 Subscription Lifecycle) testing accept/pause/resume/decline/
reopen/direction changes across two devices with browser verification.

1. Reconciliation now handles all 6 state transitions (was only 2):
   - Added pause (accepted→paused), resume (paused→accepted),
     direction change, reopen (declined→offered) to phase_metadata
   - Extracted _sync_peer_subscription() for clean state machine mirror
   - Direction sync works independently of status sync (fixes the case
     where both sides are "accepted" but have different directions)

2. Ghost pending invitations eliminated:
   - PendingInvitationCard.buildInvitations() now fetches /sync/teams
     and filters out invitations for already-known teams
   - Fixes karma-out-- folders appearing as phantom invitations after
     device acceptance (only karma-meta-- was auto-accepted)

3. Re-accept from declined works without frontend changes:
   - accept_subscription() auto-reopens declined subs before accepting
   - Frontend Re-accept button already calls accept — now it works

4. Removal delivery indicator for offline devices:
   - remove_member endpoint checks Syncthing connectivity, returns
     delivery_pending boolean when removed device is offline
   - list_members enriches removed members with delivery_pending
   - TeamMembersTab shows "delivery pending" amber badge
   - Added delivery_pending to SyncTeamMember TypeScript interface

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix each_key_duplicate crash: use composite team+project key in Sync Health {#each}
- Fix "Sessions from" card: deduplicate projects using existing projectList
- Add GET/PATCH /sync/teams/{name}/members/{device_id}/settings endpoint
- Handle direction aliases (send_only→send, receive_only→receive, null→reset)
- Remove misleading Sessions tab count (was showing sessions_sent=0)
- Fix sent_count fallback: scan all outbox folders matching member prefix

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

- Add quarantine dir to cleanup list
- Delete karma-out--* outbox/inbox directories
- Clear skill_definitions table
- Delete remote session rows from sessions table
- Clean Syncthing data directory on uninstall (certs, config, index DB)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The share_project endpoint stored encoded_name as null when not provided
in the request body. The packager then couldn't find the project directory
to package sessions. Now auto-resolves from git_identity by looking up
the sessions table.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace fragile LIKE query with exact suffix match + shortest candidate
selection. Prevents matching subdirectories or similarly-named projects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Fix list_all → list_active in reconciler — dissolved teams were
   being reconciled every 60s (bug from soft-delete change in c5c4b73)

2. Extract build_folder_config() — single source of truth replacing
   3 copy-pasted 10-key Syncthing folder config dicts in reconciler,
   sync_pending router, and folder_manager

3. Consolidate identity resolution — new resolve_encoded_name() in
   db/queries.py replaces ad-hoc SQL in sync_projects router and
   duplicated inline closure in main.py. Uses projects table first,
   falls back to sessions table with exact suffix match

4. Add team incarnation UUID — team_id field on Team domain model,
   written to team.json and removal signals. Stale signal detection
   now uses team_id match instead of fragile 60s timestamp heuristic.
   Schema v22 migration with backfill for existing teams

Also: dissolve_team() now explicitly cleans child rows (members,
projects, subscriptions) since soft-delete means CASCADE no longer
fires.

Adds docs/sync-v4-status-report.md documenting unaddressed issues,
testing timeline, UI coverage matrix, and remaining work to ship.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a structured timeline section at the top of the status report
with completed milestones (Mar 7-23), in-progress items, remaining
work to ship (3 blocking + 3 optional test sessions), and a summary
with key metrics (81 bugs fixed, 153 cross-machine steps executed,
1991 tests passing, 0 new bugs in last 2 days).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New 48-step cross-machine scenario covering last 3 blocking test gaps:
team detail (all tabs), members list, sync overview stats accuracy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The v19 migration drops and recreates sync_teams but was missing the
team_id column added by the architectural review. The v22 migration
patches it with ALTER TABLE, but if the API starts between v19 and v22
(or sync tables are created by /sync/init bypassing migrations), the
team_repo._row_to_team() crashes with IndexError: No item with key 'team_id'.

Found during cross-machine testing scenario 5 (48-step page verification).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. v22 backfill misses NULL values — SQLite ALTER TABLE ADD COLUMN gives
   existing rows NULL (not ''), so WHERE team_id = '' skips them. Fixed
   to: WHERE team_id = '' OR team_id IS NULL

2. _row_to_team() generated random uuid4() on every read when team_id
   was empty — defeating incarnation tracking since the same team gets a
   different team_id on each read. Fixed to use deterministic uuid5
   derived from team name, so reads are stable.

Found by oh-my-claudecode:analyst during post-test review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scenario 5 (48 steps) verified all 3 remaining blocking test gaps:
- Team detail (all 5 tabs, leader+member perspectives)
- Members list (search, dedup, online/offline)
- Sync overview (stats accuracy, project sync status)

84 total bugs found and fixed. 201 cross-machine steps executed.
UI completeness at ~95%. 0 blocking test sessions remain.

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

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

- /sync/members now deduplicates by device_id instead of member_tag,
  fixing duplicate entries when same device has different member_tags
  across teams. Falls back to member_tag when device_id is empty.
- Filter out dissolved teams from member aggregation — dissolved teams
  no longer contribute ghost members to the global list.
- Add machine_tag to /sync/status response alongside machine_id,
  enabling scenario YAML captures to resolve correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. received_counts always {}: indexer fallback now queries
   sync_projects.encoded_name when git_identity resolution fails

2. Stale activity on team name reuse: dissolve_team() now deletes
   sync_events and sync_removed_members (soft-delete bypassed CASCADE)

3. Subscriptions not offered to ADDED members: share_project() guard
   changed from is_active to status != REMOVED

4. Stale invitation banner after dissolution: PendingInvitationCard
   fetches teams with include_dissolved=true for knownTeams filter

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

1. PRAGMA foreign_keys = ON per connection: ensures CASCADE actually
   fires on all sync endpoints, not just during migration

2. sync_events cleanup in _auto_leave() and leave_team(): both now
   DELETE sync_events before hard-deleting the team row

3. Device unpairing in dissolve_team(): non-leader devices are now
   unpaired if not shared with other teams (matching leave/auto-leave)

4. created_at updated on team name reuse: ON CONFLICT upsert now
   includes created_at so recreated teams get fresh timestamps

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