gabriel / muse public
feat BREAKING feat/53-symlog-remote-sharing #1 / 1
AI Agent gabriel · 4 hours ago · Oct 22, 2026 · Diff

feat: muse#53 item 4 — remote symlog sharing via push/pull/bundle

Symlogs were 100% local-only until now: a fresh clone started with zero per-symbol change history. Mirrors muse#54 item 4's reflog remote-sharing precedent (same wire-conversion / idempotent-merge / transport-method / server-table shape), with one structural difference: symlog is per-symbol, not per-branch, so there is no "local vs remote-tracking namespace" split — reflog needs that split to avoid conflating a remote's ref-movement history with purely local activity, but a symbol's commit history is identical for every clone, so entries merge directly into the same local journal a muse commit would write to.

Muse CLI: - `muse push --symlog` (or `push.symlog=true` config, new [push] key alongside tags/reflog) batches every local symbol's journal entries into one POST request (unlike reflog's per-branch push, since symlog has no branch scope). - `muse pull` always best-effort fetches every symlog entry and merges directly into local journals via the new merge_remote_symlog_entries(). - `muse bundle create`/`unbundle` now also carry every local symlog entry unconditionally (bundles are a comprehensive offline transfer, so no --symlog-style opt-in needed there) — found and fixed a bug while live-verifying: `_load_bundle()` does defensive field-by-field reconstruction of the parsed msgpack dict (same as the pre-existing branch_heads field), so a "symlogs" key needed the same explicit extraction or it would be silently dropped on unbundle even though bundle create wrote it correctly. - muse/core/symlog.py: symlog_entries_to_wire() (content IDs/commit IDs pass through unchanged — unlike reflog, symlog already stores them in canonical sha256:<hex> form on disk, no stripping/normalization needed) and merge_remote_symlog_entries() (idempotent exact-raw-line comparison, appending directly into the local journal). - muse/core/transport.py: push_symlog()/fetch_symlog() on both the Protocol and HttpTransport — push batches {"symbols": {addr: [entries]}} in one request; fetch returns every symbol's entries grouped the same way. - _PushJson gained always-present symlog_pushed/symlog_skipped fields, same always-present-with-0/0-default rationale as reflog_pushed/ reflog_skipped.

MuseHub server (~/ecosystem/musehub, shipped as a companion commit): - New musehub_symlog_entries table (migration 0081), keyed by a content-hash entry_id (includes symbol_addr in the hash, unlike reflog's per-branch hash) for ON CONFLICT DO NOTHING append-only semantics. - musehub/services/musehub_symlog.py: store_symlog_entries() (batched across any number of symbols in one call, unlike reflog's single-branch call) / list_symlog_entries() (grouped by symbol addr). - wire_push_symlog/wire_get_symlog routes on the existing bare-path wire.py router.

Covered by tests/test_symlog_remote_sharing.py (11 cases: wire pass-through (no ID normalization needed, unlike reflog), core merge idempotency/per-symbol-isolation/no-collision, CLI --symlog flag + config flag + transport-not-called when disabled, pull merge + non-fatal fetch-failure handling, and a real bundle create→unbundle round trip asserting symlog_entries_merged >= 1 — this last test is what caught the _load_bundle() field-stripping bug above) plus tests/test_musehub_symlog_service.py (7 cases, musehub repo: round-trip, dedup-skip, batched multi-symbol push, per-symbol isolation, ordering, empty-batch, missing-commit_id).

Confirmed genuine RED by reverting all six touched muse-side files and re-running (ImportError: merge_remote_symlog_entries doesn't exist), then GREEN with the fix restored plus the full targeted regression suite (177 muse-side symlog/reflog/version-tag tests, zero regressions; 644 more across push/pull/config/bundle areas, zero regressions; 87 musehub-side tests including migrations + ORM/schema-match checks). Live-verified end-to-end against the real local MuseHub hub (containers restarted, migration 0081 auto-applied, schema check passed at 75 tables): created a scratch repo, pushed a symbol's journal with --symlog (confirmed idempotent on retry), pulled from a fresh clone, and confirmed the local journal received the exact original entry. Scratch repo deleted after verification.

Co-Authored-By: Claude Sonnet 5 <[email protected]>

sha256:b570925a487b44187c0441977d252906b6bf2719253bc53f96412d520cd24d9e sha
+57 ~17 −1 symbols
8 changed · 1242 in snapshot files
sha256:29e9488830315b80a0bad435ab457dc70a94fdd41111c671669912e568bd4e0c snapshot
+57
symbols added
~17
symbols modified
−1
symbol removed
8
files changed
1242
files in snapshot
0
dead code introduced
Semantic Changes 75 symbols
~ tests/test_symlog_remote_sharing.py .py 42 symbols added
+ TestBundleCarriesSymlog class class TestBundleCarriesSymlog L308–330
+ test_bundle_create_and_unbundle_round_trips_symlog method method test_bundle_create_and_unbundle_round_trips_symlog L309–330
+ TestMergeRemoteSymlogEntries class class TestMergeRemoteSymlogEntries L110–164
+ test_empty_entries_is_noop method method test_empty_entries_is_noop L162–164
+ test_idempotent_on_repeat_merge method method test_idempotent_on_repeat_merge L126–140
+ test_merges_new_entries_into_local_journal method method test_merges_new_entries_into_local_journal L111–124
+ test_separate_symbols_do_not_collide method method test_separate_symbols_do_not_collide L142–160
+ TestPullFetchesSymlog class class TestPullFetchesSymlog L233–305
+ test_pull_merges_symlog_entries method method test_pull_merges_symlog_entries L234–274
+ test_pull_symlog_fetch_failure_is_non_fatal method method test_pull_symlog_fetch_failure_is_non_fatal L276–305
+ TestPushSymlogFlag class class TestPushSymlogFlag L167–230
+ test_push_symlog_config_flag method method test_push_symlog_config_flag L211–230
+ test_push_with_symlog_calls_transport method method test_push_with_symlog_calls_transport L168–189
+ test_push_without_symlog_flag_does_not_call_transport method method test_push_without_symlog_flag_does_not_call_transport L191–209
+ TestSymlogEntriesToWire class class TestSymlogEntriesToWire L92–107
+ test_passes_through_canonical_ids method method test_passes_through_canonical_ids L93–107
+ _commit function function _commit L62–67
+ _env function function _env L35–36
+ _fake_id function function _fake_id L70–71
+ _init_repo function function _init_repo L43–55
+ _mock_push_transport function function _mock_push_transport L74–89
+ _run function function _run L39–40
+ _write function function _write L58–59
+ cli variable variable cli L31–31
+ CliRunner import import CliRunner L29–29
+ MagicMock import import MagicMock L15–15
+ NULL_CONTENT_ID import import NULL_CONTENT_ID L21–21
+ SymlogEntry import import SymlogEntry L21–21
+ annotations import import annotations L9–9
+ datetime import import datetime L11–11
+ fake_id import import fake_id L19–19
+ json import import json L12–12
+ merge_remote_symlog_entries import import merge_remote_symlog_entries L21–21
+ muse_dir import import muse_dir L20–20
+ os import import os L13–13
+ patch import import patch L15–15
+ pathlib import import pathlib L14–14
+ pytest import import pytest L17–17
+ read_symlog import import read_symlog L21–21
+ symlog_entries_to_wire import import symlog_entries_to_wire L21–21
+ symlog_path import import symlog_path L21–21
+ runner variable variable runner L32–32
~ docs/issues/symlog-followup.md .md 1 symbol added, 1 symbol removed, 2 symbols modified
− Remote symlog sharing section 4. Remote symlog sharing L78–87
+ Remote symlog sharing ✅ IMPLEMENTED section 4. Remote symlog sharing ✅ IMPLEMENTED L78–110
~ muse/cli/commands/bundle.py .py 4 symbols added, 4 symbols modified
+ list_symlog_symbols import import list_symlog_symbols L74–74
+ merge_remote_symlog_entries import import merge_remote_symlog_entries L74–74
+ read_symlog import import read_symlog L74–74
+ symlog_entries_to_wire import import symlog_entries_to_wire L74–74
~ muse/cli/commands/pull.py .py 1 symbol added, 1 symbol modified
+ merge_remote_symlog_entries import import merge_remote_symlog_entries L65–65
~ run
~ muse/cli/commands/push.py .py 3 symbols added, 3 symbols modified
+ list_symlog_symbols import import list_symlog_symbols L80–80
+ read_symlog import import read_symlog L80–80
+ symlog_entries_to_wire import import symlog_entries_to_wire L80–80
~ run
~ muse/core/symlog.py .py 2 symbols added
+ merge_remote_symlog_entries function function merge_remote_symlog_entries L548–619
+ symlog_entries_to_wire function function symlog_entries_to_wire L527–545
~ muse/core/transport.py .py 4 symbols added, 2 symbols modified
+ fetch_symlog method method fetch_symlog L1379–1401
+ push_symlog method method push_symlog L1357–1377
+ fetch_symlog method method fetch_symlog L570–584
+ push_symlog method method push_symlog L547–568
Files Changed
+1 ~7
1242 in snapshot
← Older Oldest on feat/53-symlog-remote-sharing
All commits
Newer → Latest on feat/53-symlog-remote-sharing

0 comments

No comments yet. Be the first to start the discussion.

To add a comment, use the Muse CLI: muse hub commit comment sha256:b570925a487b44187c0441977d252906b6bf2719253bc53f96412d520cd24d9e --body "your comment"