Skip to content

Extend extra session roots to all views, add machine badge (2/2, depends on #115) - #116

Open
crazy-daddy wants to merge 6 commits into
JayantDevkar:mainfrom
crazy-daddy:feature/extra-roots-coverage
Open

crazy-daddy wants to merge 6 commits into
JayantDevkar:mainfrom
crazy-daddy:feature/extra-roots-coverage

Conversation

@crazy-daddy

Copy link
Copy Markdown
Contributor

Important

Depends on #115 — merge that first. This branch is stacked on #115 and needs its services/session_roots.py and the sessions.source_root column. Until #115 is merged, the diff here also shows its commit: review only the second commit, "extend extra session roots to all views, add machine badge" (or compare feature/extra-roots-core...feature/extra-roots-coverage on the fork).

Summary

Part 2 of 2 of extra session roots (see #115 for the feature and configuration). #115 indexes synced sessions and makes the project list, project pages and session lookup see them. This PR covers every remaining place that read only the local ~/.claude/projects dir, and labels synced sessions with the machine they came from.

Changes

  • Remaining call sites read synced dirs through the helpers from Merge sessions from other machines via extra session roots (1/2: core) #115 (session_jsonl_path, project_dirs_for, Project.session_dirs):
    • sessions router: global session browser (JSONL fallback), continuation lookup, prefix lookup, subagent counts
    • projects router: slug cache, UUID/prefix/slug lookup, memory
    • session chain resolver, title cache, memory index, cron fires, agent analytics, file history, session filter, backfill_titles script
  • Synced copies of one session count once; a file that's partial or locked mid-sync is skipped without hiding the rest of its machine's sessions
  • agent_analytics and backfill_titles use settings.projects_dir instead of a hardcoded ~/.claude/projects
  • SessionSummary.machine (from sessions.source_root, null for this machine) and a machine badge (laptop icon + root name) on session cards, global session cards and the conversation header, next to the existing Desktop badge

With no roots configured, every path behaves as before.

Testing

  • Full API suite: 1951 passed; the same 12 Windows-only failures as on main
  • npm run check: 0 errors (13 existing warnings, none in changed files)
  • ruff check: no new findings

@JayantDevkar JayantDevkar left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the second commit only, as asked. The call-site changes are clean: the no-roots path matches main at every site I checked, no O(1) lookup became a scan, the dedupe and the partial-file skip are there, and the skip only wraps the remote loop so a corrupt local JSONL still surfaces as before. The badges copy the Desktop badge's classes and tokens and render nothing when machine is null. No blockers; this merges after #115 once the following are in.

Should fix

  • No tests for this commit: test_session_roots.py has the same 30 tests as #115. Add, with the real temp-dir fixture: browser dedupe (a session synced to two dirs counts once), a partial or unreadable remote file skipped while the rest still list, machine set on browser and detail responses for a synced session and null for a local one, and session_jsonl_path / project_dirs_for returning the local path with no roots.
  • routers/projects.py:561-564: the "SQLite returned 0 sessions, fall back to disk" check globs only project.project_dir, so a remote-only project not yet indexed gets no fallback. Use project.list_session_paths().
  • services/session_roots.py:569-574: with roots configured, machine_for_path / root_for_path call resolve() on the file and on every root for every session row. Cache the resolved base on SessionRoot.
  • services/session_lookup.py:70-84: a detail request for a synced session scans every local project dir before trying the remote index. Check sessions.source_root first, or try the remote index first when roots exist. Worth doing, not required.

Nits

  • routers/sessions.py:1880-1882: an exact remote hit returns immediately and drops local prefix matches already collected, which hides ambiguity for short prefixes.
  • services/session_relationships.py:57-59: when the starting dir is a synced one, the remote copy wins duplicates; everywhere else local wins. Put the local dir first.
  • routers/skills.py:621 and :715, routers/tools.py:134, routers/agents.py:476 build summaries without machine, so synced sessions show no badge there. Say so in the PR or wire them up.
  • routers/agent_analytics.py:60-61 still hardcodes ~/.claude/agents; :92-97 (appending remote canonicals) is nearly dead code.
  • SessionCard.svelte:226-243 stacks the Desktop and machine badges as two rows in the compact layout; GlobalSessionCard.svelte:219-235 puts them on one row. Match them.
  • One-line comments: api/scripts/backfill_titles.py:90-91, and the two-line docstring summary at api/db/queries_shells_cron.py:212-213.

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.
Part 2 of 2 (stack). Depends on feature/extra-roots-core (part 1):
branched from it and needs its services/session_roots.py and the
sessions.source_root column. Merge part 1 first; until then this
branch's diff against main also shows part 1.

- remaining call sites read synced dirs via session_jsonl_path /
  project_dirs_for / Project.session_dirs: sessions and projects routers
  (slug cache, prefix lookup, continuation lookup, JSONL fallback),
  session chain resolver, title cache, memory, cron fires, agent
  analytics, history, session filter, backfill_titles script
- synced copies of one session count once; a partial file mid-sync is
  skipped without hiding the rest of its root
- agent_analytics and backfill_titles use settings.projects_dir instead
  of a hardcoded ~/.claude/projects
- SessionSummary.machine (from sessions.source_root) and a machine badge
  on session cards and the conversation header
Should fix:
- Tests with the temp-dir fixture: browser dedupe of a session synced to
  two dirs, an unreadable remote file skipped while the rest list, machine
  set on browser and detail responses for a synced session and null for a
  local one, session_jsonl_path / project_dirs_for local with no roots,
  index-backed lookup, short-id prefix hits across dirs, local-first
  relationship resolver.
- projects.py: the "SQLite returned 0 sessions" disk fallback uses
  project.list_session_paths(), so a remote-only project gets it too.
- session_lookup.py: with roots configured, find_session_with_project asks
  the index for project and source_root first (one stat) and only scans
  when the index does not know the session or its file moved. A local copy
  wins even when the index still says synced.

Nits:
- skills, tools and agents summaries carry machine: the six
  query_sessions_by_* SELECTs include s.source_root, the JSONL fallback in
  skills uses machine_for_path.
- session_relationships: local dir first when the start dir is a synced one.
- agent_analytics: agents dir from settings.claude_base; the remote
  canonicals loop (dead: those projects' .claude/agents live on the other
  machine) is gone.
- SessionCard compact layout puts Desktop and machine badges on one row,
  matching GlobalSessionCard.
- One-line comments in backfill_titles.py and queries_shells_cron.py.
- _find_session_files keeps returning an exact remote hit alone, like the
  local loop: an exact filename only exists for a full UUID, where prefix
  hits are moot, and the short-id resolver would otherwise pick by mtime.
@crazy-daddy

Copy link
Copy Markdown
Contributor Author

Rebased onto the current #115 tip (8e8e445) and pushed e995732 for the review.

Should fix

  • Tests, all on the temp-dir fixture (test_session_roots.py, "Views" section): a session synced to two dirs lists once with machine null for the local copy and the root name for a synced one; an unreadable remote file (a directory named r2.jsonl, which opens fine and fails on read, the shape of a file the sync client holds) is skipped while the rest still list; machine on the detail response for synced vs local; session_jsonl_path / project_dirs_for return the local path with no roots; plus index-backed lookup, short-id prefix hits across dirs, and the relationship resolver's local-first order.
  • routers/projects.py: the "SQLite returned 0 sessions" fallback checks project.list_session_paths(), so a remote-only project not yet indexed gets it.
  • services/session_lookup.py: with roots configured, find_session_with_project asks the index for project_encoded_name and source_root first and stats that one path; it scans only when the index does not know the session or the file moved. A local copy wins even when the index still says synced.

Nits

  • machine now set in routers/skills.py (both SQLite builders and the JSONL fallback), routers/tools.py and routers/agents.py; the six query_sessions_by_* SELECTs include s.source_root.
  • session_relationships.py: local dir first when the starting dir is a synced one.
  • agent_analytics.py: agents dir from settings.claude_base; the remote-canonicals loop is gone (those projects' .claude/agents live on the other machine, so it could never find anything).
  • SessionCard.svelte compact layout: Desktop and machine badges on one row, same markup as GlobalSessionCard.svelte.
  • One-line comments in backfill_titles.py and queries_shells_cron.py.
  • _find_session_files: I kept the immediate return on an exact remote hit rather than appending it to the local prefix hits. The short-id resolver picks among candidates by mtime, so a newer local prefix match would beat an exact id. An exact filename only exists for a full UUID, where there is nothing ambiguous to surface; short prefixes still collect hits from every dir.

Verified: test_session_roots.py 57 passed; full API suite 2025 passed with the same 6 Windows-only failures as main deselected; ruff clean on the CI scope; npm run check and npm run lint 0 errors.

@crazy-daddy
crazy-daddy force-pushed the feature/extra-roots-coverage branch from fd15a2c to e995732 Compare October 21, 2026 19:32
The models-only CI job installs no web deps; the test imports
routers.sessions, so it skips there like the endpoint tests do.
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