feat: peer-to-peer session sharing via Syncthing#45
Open
JayantDevkar wants to merge 379 commits intomainfrom
Open
feat: peer-to-peer session sharing via Syncthing#45JayantDevkar wants to merge 379 commits intomainfrom
JayantDevkar wants to merge 379 commits intomainfrom
Conversation
47dbda4 to
9f85491
Compare
…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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
/sync— the setup wizard detects Syncthing and walks you through initializationFour core concepts
jayant.macbook). Same person on two machines = two members.org/repo). You choose what to share.What you can do
/teampage — add members via join codes, share projects, view activityWhat gets synced (and what doesn't)
Synced: session conversations, tool usage, token stats, subagent activity, session metadata
Never synced: your source code, secrets,
.envfiles, anything outside~/.claude/projects/, anything from projects you haven't sharedArchitecture
The sync system is built in clean layers:
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
What's in the PR
Backend —
api/api/domain/(5 files)api/db/schema.pyapi/repositories/(5 files)api/services/syncthing/(3 files)api/services/sync/(5 files)api/routers/sync_*.py(5 files)api/services/watcher_manager.pyFrontend —
frontend//syncpage/teampage/team/[name]api-types.tsupdated with SyncTeam, SyncTeamMember, SyncSubscription, SyncEventCLI —
cli/karma/sync-config.json)Documentation —
docs/about/Numbers
+65,615 / -2,037linesTest plan
svelte-check— 0 errors)🤖 Generated with Claude Code