Repository navigation
Merge sessions from other machines via extra session roots (1/2: core) - #115
crazy-daddy wants to merge 3 commits into
Conversation
JayantDevkar
left a comment
There was a problem hiding this comment.
Thanks for this. I read the whole thing and I like the direction: read-only roots, a canonical-name mapping layer and a cached single-flight registry are small, isolated and reversible, and this is a much better fit for the project than a daemon. I want to build on it. Two things block the merge, and a handful should change with them.
Blockers
- Karma writes into
~/.claude/projectseven with no roots configured.db/indexer.py:963-968callswrite_sidecarfor every git project on every index pass, so every existing user gets akarma-project.jsonin each project dir plus a git-root lookup per project per pass. That directory is Claude Code's, and the PR body says the no-roots path is unchanged. Gate the sidecar write (and the stale-sidecar delete atsession_roots.py:239-243) on at least one configured root. - The folder-name guess (rule 5) is on by default and only half-guarded.
session_roots.py:394rejects only when both sides have a known, different remote. The remote side only has an identity if it has a sidecar, which machines without Karma or the #117 hook never do, and on the first index pass local sidecars don't exist yet either (indexer.py:195vs:966). So a remoteapi,webordemofolder merges silently into whichever local project shares the name. Defaultmatch_by_nameto off, or require identity on both sides.
Should fix
- A malformed or mid-edit
extra-roots.jsonwipes every remote row:_load_root_entries(session_roots.py:157-165) logs and returns an empty list,_sync_extra_rootsclears the cache first (indexer.py:718), and_cleanup_stale_sessionsthen treats every root as removed (indexer.py:780-783). Raise on parse failure so_refreshkeeps the previous roots, and skip the removed-root deletion unless the config loaded cleanly. - A mid-copy JSONL can be indexed truncated and never re-read:
sync_project(indexer.py:316) compares mtime only.cp -pand SMB can stamp the final mtime before the data lands. For remote files also comparejsonl_size(already stored). - Rule 2 with two local clones of the same repo and subdir:
by_identity.setdefault(session_roots.py:316) picks whichever comes first in directory order. Treat a duplicated key as ambiguous and fall through. GET /extra-roots(routers/extra_roots.py:19) exposes absolute host paths with no localhost guard, and?refresh=truemutates state on a GET. Reuse_require_localfromrouters/desktop_app.pyand make refresh a POST.- Root validation (
session_roots.py:169-189): also reject duplicate resolved paths, a root nested inside or containing the local~/.claude, and a path that is a file. Keep missing paths allowed, offline roots are intentional. path_map(session_roots.py:278-285): prefix matching is case-insensitive on POSIX too, so/home/Meand/home/mecollide, and a source of/normalises to an empty prefix that matches everything.
Nits
- Multi-line comment blocks:
config.py:63-68,session_roots.py:75-76and95-98,schema.py:819-820,indexer.py:313-315. We keep code comments to one line here. root_for_path(session_roots.py:563-578) resolves the file and every root on eachSession.from_pathonce roots exist; cache the resolved bases onSessionRoot.- Tests worth adding: malformed roots file, nested root, partial file, the endpoint, "no roots means no sidecar write".
The cachetools CI failure on the guard job was ours (a minimal pip list in the workflow); fixed on main in #120, so a rebase clears it. Once the two blockers are in I'll go through #116 and #117 with it; I've left notes on both.
Part 1 of 2 (stack). Part 2, feature/extra-roots-coverage, builds on this branch and extends coverage to the remaining views plus a machine badge. The sidecar hook (feature/extra-roots-hook) is independent. Users sync other machines' ~/.claude dirs here with any tool (Syncthing, OneDrive, rsync, ...) and list them in ~/.claude_karma/extra-roots.json or CLAUDE_KARMA_EXTRA_ROOTS. Karma reads them read-only; no sync daemon. Each remote project dir joins a local project by the first matching rule: project_map, karma-project.json sidecar (git identity + subdir), path_map prefix rewrite, same encoded name, unique folder name (guess). Unmatched dirs show as remote-only projects. Remote worktree dirs follow their project. - services/session_roots.py: root config, sidecars, matching, TTL cache - indexer: third pass for remote dirs, sessions.source_root column (no reindex), local copy wins, first root wins, offline roots keep their rows - Project.session_dirs: listing, lookup and agents span all dirs - session_lookup falls back to remote dirs - local git projects get a sidecar on every index pass - GET /extra-roots shows how each remote dir was matched - SETUP.md: Multiple Machines section
Blockers: - Write karma-project.json sidecars only while at least one extra root is configured. Without roots Karma no longer touches ~/.claude/projects. - match_by_name now defaults to off; the folder-name guess is opt-in per root. Should fix: - A malformed or mid-edit extra-roots.json raises in _load_root_entries; _refresh keeps the previous roots and flags config_ok=False, and the indexer skips the removed-root cleanup until the config loads cleanly. The indexer calls invalidate() (keeps the fallback) instead of clear_cache(). - Remote JSONL files are re-indexed when their size changed even if the mtime did not (cp -p, SMB stamp the final mtime before the data lands). - A sidecar identity claimed by two local clones is ambiguous and no longer matches rule 2. - GET /extra-roots is localhost-only via _require_local; refresh moved to POST /extra-roots/refresh. _require_local takes a subject for the 403 text. - Root validation rejects duplicate resolved paths, roots overlapping the local ~/.claude, and paths that are files. Missing paths stay allowed. - path_map: "/" (empty prefix) is ignored; prefixes are case-insensitive only for Windows paths (drive letter or UNC). Nits: - One-line comments in config.py, session_roots.py, schema.py. - SessionRoot caches its resolved base; root_for_path no longer resolves every root per session file. - Tests: malformed roots file, non-list file, nested/duplicate/file roots, path_map edge cases, ambiguous identity, partial remote file, endpoints, no roots means no sidecar write.
- path_map: a bare drive source ("C:/", normalised to "C:") is treated as a
Windows prefix and matched case-insensitively.
- SessionRoot resolves its base in __post_init__, so a root built without
resolved_base still matches symlinked paths in root_for_path.
- Tests: no roots also means no stale-sidecar delete; a malformed roots file
on a cold start (no previous roots in memory) keeps remote rows; drive-root
path_map case.
0779a03 to
8e8e445
Compare
|
Rebased onto main (picks up #120) and pushed two commits on top: Blockers
Should fix
Nits
One known edge I left as is: a malformed roots file on a cold start also drops roots supplied via Verified: |
|
On the two red checks: neither is in code this PR touches, and both look environmental.
I cannot rerun the failed jobs from the fork (admin rights). A rerun on your side should clear them; happy to push an empty commit instead if you prefer, though that moves #116's base again. |
|
Addendum: the macOS 3.10 |
Summary
Shows Claude Code sessions from several computers in one Karma dashboard, without Karma doing any syncing itself.
Users copy each other machine's
~/.claudedir onto the Karma machine with whatever they already use (Syncthing, OneDrive, Dropbox, rsync, a network share) and list those copies as extra roots:Karma reads extra roots read-only. This is a much smaller alternative to the Syncthing-based approach in #85: no daemon, no teams or pairing, no schema chain.
This is part 1 of 2. #116 is stacked on this branch: it extends coverage to the remaining views and adds a machine badge, and should merge after this one. #117 (independent, any order) adds an optional hook for exact matching on machines that don't run Karma.
How remote projects are matched
A project dir from another machine joins a local project by the first rule that matches:
project_map: explicit{remote dir: local dir}in the root configkarma-project.jsonsidecar: same git remote and subdir within the repopath_map: prefix rewrite of the remote project pathmatch_by_name, on by default; rejected when both sides have known, different git remotes)Anything unmatched shows as its own project. Remote worktree dirs follow their main project. Karma writes a sidecar into each local git project dir on every index pass, so machines that also run Karma get exact matching automatically.
Changes
api/services/session_roots.py(new): root config, sidecars, matching, cached mapping with a single-flight rebuildsessions.source_rootcolumn (added without a reindex). A session present locally wins over a synced copy, the first root wins across roots, and an offline root (unmounted drive, paused sync) keeps its rowsProject.session_dirs: session listing, lookup and agents span local and synced dirs; remote-only projects appear in the project listsession_lookupfalls back to synced dirsGET /extra-roots[?refresh=true]: shows each remote dir and which rule matched itSETUP.md: new "Multiple Machines" sectionWith no roots configured, behavior is unchanged: the registry returns early without disk I/O.
Testing
api/tests/test_session_roots.py: matching rules, config validation, sidecars,Projectacross dirs, indexer merge/dedupe/offline/removed roots, failure handlingmainon Windows (live session store, terminal focus); unrelatedruff check: no new findings