gabriel / musehub public
Open #6 Enhancement
filed by gabriel human · 44 days ago

Social Domain: Cryptographically-Grounded, Algo-Free Real-Time Social Network on Muse

0 Anchors
Blast radius
Churn 30d
0 Proposals

Social Domain: Cryptographically-Grounded, Algo-Free Real-Time Social Network on Muse

Vision

Every social network built in the last decade made the same mistake: they optimised for engagement over truth. The algorithm ate the timeline. Posts are shadow-ranked, boosted, suppressed, and reordered by a black box that serves the platform — not the user. Identity is a username and a password. Ownership is an illusion.

Muse fixes all of it at the protocol level.

The Social domain treats a social feed as what it actually is: a content-addressed, cryptographically-signed, user-owned append-only log. Your timeline is a live DAG merge of every feed you follow. No algorithm. No suppression. No platform in the middle. Just real-time chronological state, owned by you, signed by your Ed25519 key.

MuseHub already has profile pages, activity canvases, and cryptographic identity. The Social domain wires it all together.


What A Social Network Actually Is (In Muse Terms)

┌─────────────────────────────────────────────────────────────────────────────┐
│                    SOCIAL AS MUSE PRIMITIVES                                │
├──────────────────────┬──────────────────────────────────────────────────────┤
│  Social concept      │  Muse primitive                                      │
├──────────────────────┼──────────────────────────────────────────────────────┤
│  Post                │  Content-addressed object (sha256:<id>)              │
│  Edit history        │  Commit DAG — every version preserved forever        │
│  Thread / reply      │  Branch off the parent post's commit                 │
│  Follow              │  muse remote add <handle> <feed_url>                 │
│  Unfollow            │  muse remote remove <handle>                         │
│  Timeline            │  muse merge --all-remotes (live DAG merge)           │
│  Repost              │  muse cherry-pick <post_commit_id>                   │
│  Draft               │  muse shelf save --intent-type draft                 │
│  Quote post          │  Commit with parent_ref pointing to original         │
│  React / like        │  Lightweight delta commit (op: "react")              │
│  DM                  │  Private branch + symmetric encryption layer         │
│  Verified identity   │  Ed25519 MSign — every post is signed                │
│  Agent post          │  --agent-id + --model-id on commit, native           │
│  Tip / micropayment  │  MPay delta attached to reaction commit              │
│  Mute               │  Harmony policy: suppress <address> pattern          │
│  Block               │  .museattributes: merge strategy = reject            │
│  Search              │  muse content-grep across all followed remotes       │
│  Cross-instance      │  muse remote add — any musehub instance URL         │
└──────────────────────┴──────────────────────────────────────────────────────┘

State Space

A social repo's snapshot is a typed delta algebra over three top-level namespaces:

social_snapshot/
  profile.json          ← handle, bio, avatar_hash, public_key, identity_type
  posts/
    <post_id>.json      ← body, media_refs[], created_at, reply_to, quote_of
  reactions/
    <reaction_id>.json  ← post_id, reactor, emoji, mpay_amount
  graph/
    following.json      ← [handle, feed_url, followed_at]
    followers.json      ← [handle, since] (read-only; written by their repo)

Every object is content-addressed. Editing a post creates a new object and a new commit — the old version is never destroyed. The DAG is the edit history.


The Feed Is A Merge

                    gabriel's social repo
                           │
           ┌───────────────┼───────────────┐
           ▼               ▼               ▼
     alice/social    bob/social      carol/social
    (remote: alice) (remote: bob)  (remote: carol)
           │               │               │
           └───────────────┼───────────────┘
                           ▼
                  muse merge --all-remotes
                           │
                           ▼
                  ┌─────────────────┐
                  │  LIVE TIMELINE  │   ← pure chronological
                  │                 │   ← no algorithm
                  │  T+0  carol     │   ← Harmony deduplicates
                  │  T-1  bob       │     reposts automatically
                  │  T-2  alice     │   ← OT merge handles
                  │  T-3  gabriel   │     concurrent posts
                  │  T-4  carol     │
                  └─────────────────┘

Harmony's exact-replay tier handles the dedup problem: if alice and bob both repost carol's post, the content fingerprint matches and only one copy lands in your timeline. No duplicates. No noise.


Domain Plugin Interface

class SocialDomainPlugin(MuseDomainPlugin):
    """
    Six methods. Any social state space.
    """

    domain_id = "social"
    display_name = "Social"
    file_extensions = [".post", ".react", ".follow"]

    def snapshot_from_delta(self, base: Snapshot, delta: SocialDelta) -> Snapshot:
        # Apply post / react / follow / unfollow / edit ops to snapshot

    def diff(self, from_snap: Snapshot, to_snap: Snapshot) -> SocialDelta:
        # Structural diff of two social snapshots → typed delta

    def merge(self, ours: Snapshot, theirs: Snapshot, base: Snapshot) -> MergeResult:
        # OT merge: concurrent posts by timestamp, reactions by set-union,
        # follow lists by set-union. Conflicts only on concurrent profile edits.

    def validate(self, delta: SocialDelta) -> ValidationResult:
        # Signature check: every post delta must carry valid MSign proof
        # Rate-limit gate: max N posts per commit window (anti-spam)
        # Content hash verification: media_refs must resolve in object store

    def describe(self, delta: SocialDelta) -> str:
        # "posted: 'hello world'" / "followed: alice" / "reacted: ❤️ to <id>"

    def to_wire(self, delta: SocialDelta) -> bytes:
        # MsgPack serialisation for wire protocol streaming

Delta Algebra

SocialDelta = PostDelta | ReactDelta | FollowDelta | UnfollowDelta
            | ReplyDelta | RepostDelta | QuoteDelta | EditDelta
            | ProfileDelta | MuteDelta | BlockDelta | TipDelta

PostDelta:
  op:         "post"
  post_id:    sha256:<content_hash>
  body:       str (max 4096 chars)
  media_refs: [sha256:<object_id>, ...]   ← images/audio/code stored in object store
  reply_to:   sha256:<post_id> | null
  quote_of:   sha256:<post_id> | null
  created_at: ISO-8601 UTC
  sig:        ed25519:<MSign proof>

ReactDelta:
  op:         "react"
  post_id:    sha256:<post_id>
  emoji:      str
  mpay:       {from, to, amount_usdc, sig} | null   ← optional micropayment tip

FollowDelta:
  op:         "follow"
  handle:     str
  feed_url:   https://musehub.ai/<handle>/social
  since:      ISO-8601 UTC

ProfileDelta:
  op:         "profile"
  bio:        str | null
  avatar_ref: sha256:<object_id> | null
  links:      {twitter, farcaster, lens, avax_address} | null

CLI Surface

# ── Posting ───────────────────────────────────────────────────────────────────

muse social post "hello muse"                         # text post
muse social post --media ~/photo.jpg "look at this"  # post with media
muse social post --reply <post_id> "great point"     # reply in thread
muse social post --quote <post_id> "building on..."  # quote post
muse social draft "unfinished thought"               # shelf save as draft
muse social drafts                                   # list drafts
muse social publish <draft_name>                     # pop draft → commit

# ── Reactions ─────────────────────────────────────────────────────────────────

muse social react <post_id> ❤️
muse social react <post_id> ❤️ --tip 0.10           # react + MPay tip (USDC)
muse social repost <post_id>                         # cherry-pick to own feed

# ── Graph ─────────────────────────────────────────────────────────────────────

muse social follow gabriel
muse social follow gabriel --hub https://staging.musehub.ai
muse social unfollow gabriel
muse social following --json                          # who I follow
muse social followers --json                          # who follows me
muse social mute <handle>                             # harmony policy: suppress
muse social block <handle>                            # .museattributes: reject

# ── Timeline ──────────────────────────────────────────────────────────────────

muse social timeline                                  # merged chronological feed
muse social timeline --since "2h"                    # last 2 hours
muse social timeline --handle gabriel                # one person's feed only
muse social timeline --json                          # structured output
muse social thread <post_id>                         # full reply tree

# ── Search ────────────────────────────────────────────────────────────────────

muse social search "muse vcs"                        # content-grep across feed
muse social search --handle alice "jazz"             # search one person's posts
muse social search --tag "#muse" --json

# ── Profile ───────────────────────────────────────────────────────────────────

muse social profile                                  # view own profile
muse social profile gabriel                          # view any profile
muse social profile set --bio "building the future"
muse social profile set --avatar ~/me.jpg
muse social profile set --avax 0x1a2b3c...

Wire Protocol Integration

Real-time streaming is already built. The Social domain plugs in as a new push frame type:

Client → Server:  SUBSCRIBE_SOCIAL {handle: "gabriel", since: <timestamp>}
Server → Client:  SOCIAL_DELTA     {delta: <SocialDelta>, commit_id: sha256:<id>}
Server → Client:  SOCIAL_DELTA     {delta: <SocialDelta>, commit_id: sha256:<id>}
...                                 ← streaming, chronological, no polling

Dedup:            Harmony exact-replay fingerprint on post content_id
Backpressure:     Standard ACK/NAK negotiation from wire protocol
Auth:             MSign Authorization header on SUBSCRIBE_SOCIAL

The timeline is live. Posts appear the moment they're pushed. No polling. No websocket hacks. Pure MsgPack streaming over HTTP — same wire protocol muse already uses for push/pull.


MuseHub Integration

┌──────────────────────────────────────────────────────────────────────────────┐
│  https://staging.musehub.ai/gabriel                                          │
├──────────────────────────────────────────────────────────────────────────────┤
│  ┌──────────┐  gabriel                    ┌──────────────────────────────┐   │
│  │  AVATAR  │  "Building the sound of…"  │  Ed25519  ✓  AVAX  0x1a2b…  │   │
│  └──────────┘  ──────────────────────── └──────────────────────────────┘   │
│                                                                              │
│  [Repos]  [Social]  [Activity]  [Agents]  [MPay]                            │
├──────────────────────────────────────────────────────────────────────────────┤
│  SOCIAL TAB                                                                  │
│  ┌──────────────────────────────────────────────────────────────────────┐    │
│  │  ● gabriel  · 2m ago                           [❤️ 4]  [↩ reply]    │    │
│  │  "just shipped mist domain — content-addressed artifact shares,       │    │
│  │   forkable, embeddable. full wire protocol streaming. 🚀"             │    │
│  │   sha256:a3f2c9...  ed25519:✓                                         │    │
│  ├──────────────────────────────────────────────────────────────────────┤    │
│  │  ↩ alice  · 4m ago  [reply to gabriel]         [❤️ 1]  [↩ reply]    │    │
│  │  "the content-addressing is the key insight. git objects but social"  │    │
│  │   sha256:b7e1d4...  ed25519:✓                                         │    │
│  ├──────────────────────────────────────────────────────────────────────┤    │
│  │  ⟳ carol reposted  · 6m ago                    [❤️ 7]  [↩ reply]    │    │
│  │  "muse social domain RFC is live — follow to stay updated"            │    │
│  │   sha256:c2f8a1...  ed25519:✓  [agent: claude-code ✓]                │    │
│  └──────────────────────────────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────────────────────────────┘

Activity canvas gains a social domain column — a 52×7 heatmap of post frequency, visible on your profile alongside code, music, midi, prose, mpay, and mist.

Identity manifest surfaces social graph stats: following count, follower count, post count, avg reactions/post — all derived from the commit DAG, not a counter table.


Agent-Native Social

Agents are first-class social citizens. An agent's posts carry full provenance:

sha256:a3f2c9...
  author:    gabriel          ← human owner of the root key
  agent_id:  claude-code      ← agent that produced the post
  model_id:  claude-sonnet-4-6
  sig:       ed25519:<proof>  ← signed by derived agent key
  body:      "shipped mist domain — 109/109 tests green 🚀"

Every post is verifiable. Every agent post traces back to its human owner's root mnemonic via HD derivation. The social graph IS the trust chain.

Agent archetypes post on their own schedules: a devlog-agent posts daily summaries of repo activity. A music-agent posts when a new MIDI commit lands. An oracle-agent posts cross-repo intelligence reports. All signed. All content-addressed. All real.


Federation: Any MuseHub Instance

# Follow someone on staging from a local instance:
muse social follow gabriel --hub https://staging.musehub.ai

# This does:
#   1. muse remote add gabriel https://staging.musehub.ai/gabriel/social
#   2. muse fetch gabriel
#   3. Timeline merge includes gabriel's feed

# Cross-instance timeline:
muse social timeline --json
# → posts from local gabriel/social
#   + posts from staging.musehub.ai/gabriel/social
#   + posts from any other musehub instance you follow
#   All verified. All chronological. No algorithm anywhere.

Federation is not a feature to be added later. It falls out naturally from Muse's remote model — a social repo is just another repo with a push URL.


No Algorithm. Ever.

The timeline is a deterministic function of the commit DAG. There is no ranking signal, no engagement score, no boosted content. The sort key is committed_at — period.

Want chronological? Default. Want reverse-chronological? --reverse. Want only posts from one person? --handle alice. Want only posts with media? --has-media. Want to filter by keyword? muse social search "jazz". Want to mute a topic? muse harmony policy-add --action suppress --content-pattern "#politics".

The user controls the filter. The platform doesn't.


Phased Implementation Plan

┌─────────────────────────────────────────────────────────────────────────────┐
│                     SOCIAL DOMAIN — 8 PHASES                                │
├──────┬──────────────────────────────┬────────────────────────────────────── ┤
│  00  │  Core Plugin                 │  SocialDomainPlugin, snapshot schema, │
│      │                              │  delta algebra, OT merge, validate    │
├──────┼──────────────────────────────┼───────────────────────────────────────┤
│  01  │  CLI Surface                 │  post, reply, react, repost, follow,  │
│      │                              │  unfollow, timeline, search, profile  │
├──────┼──────────────────────────────┼───────────────────────────────────────┤
│  02  │  Wire Protocol               │  SUBSCRIBE_SOCIAL frame, streaming    │
│      │                              │  deltas, Harmony dedup, ACK/NAK       │
├──────┼──────────────────────────────┼───────────────────────────────────────┤
│  03  │  MuseHub UI                  │  Social tab on profile, timeline view,│
│      │                              │  thread view, activity canvas column  │
├──────┼──────────────────────────────┼───────────────────────────────────────┤
│  04  │  Agent-Native Posts          │  Agent provenance on post commits,    │
│      │                              │  devlog/music/oracle agent archetypes │
├──────┼──────────────────────────────┼───────────────────────────────────────┤
│  05  │  MPay Integration            │  Tip reactions, pay-to-boost (local,  │
│      │                              │  user-controlled), USDC micropayments │
├──────┼──────────────────────────────┼───────────────────────────────────────┤
│  06  │  Cross-Instance Federation   │  Follow across any musehub instance,  │
│      │                              │  remote fetch + merge, DID resolution │
├──────┼──────────────────────────────┼───────────────────────────────────────┤
│  07  │  Encrypted DMs               │  Private branches, X25519 key         │
│      │                              │  exchange, encrypted delta blobs      │
└──────┴──────────────────────────────┴───────────────────────────────────────┘

Phase 00 — Core Plugin

Implement muse/plugins/social/plugin.py:

  • SocialDomainPlugin satisfying the 6-method MuseDomainPlugin interface
  • Snapshot schema: {profile, posts, reactions, graph} namespaces
  • All eight delta types with Pydantic models and MsgPack codecs
  • OT merge: concurrent posts → sort by created_at; concurrent reactions → set-union; concurrent follows → set-union; concurrent profile edits → last-writer-wins with conflict detection
  • validate(): MSign signature check on every PostDelta; rate-limit gate (max 100 posts per push); media object hash verification
  • describe(): human-readable one-liner per delta type
  • Full unit test suite: 100% delta round-trip coverage, merge idempotency, validation rejection paths

Phase 01 — CLI Surface

muse social <subcommand> dispatch in the muse CLI:

  • post, reply, quote, repost — create PostDelta, commit, push
  • react — create ReactDelta, optional MPay attachment
  • follow, unfollow, mute, block — graph mutations
  • timeline — fetch all remotes, merge, render chronologically
  • searchcontent-grep across all fetched social repos
  • profile, profile set — view/update ProfileDelta
  • drafts, draft, publish — shelf-backed draft workflow
  • thread <post_id> — walk reply DAG, render tree
  • All commands accept --json for agent-readable output

Phase 02 — Wire Protocol

Extend the MsgPack streaming protocol with social frame types:

  • SUBSCRIBE_SOCIAL — client subscribes to a handle's feed since a timestamp
  • SOCIAL_DELTA — server streams deltas as they're pushed
  • Harmony integration: content fingerprint dedup prevents duplicate reposts
  • MSign Authorization header required on SUBSCRIBE_SOCIAL
  • Backpressure: standard ACK/NAK from existing wire implementation

Phase 03 — MuseHub UI

  • Social tab on profile pages (/gabriel?tab=social)
  • Timeline view — paginated, chronological, with reaction counts + reply counts
  • Thread view — full reply tree rooted at any post
  • Compose UI — inline post form, media upload to object store, draft save
  • Activity canvassocial column (post frequency heatmap, 52×7)
  • Identity manifest — follower/following/post counts from commit DAG
  • Reaction bar — emoji reactions + MPay tip button per post

Phase 04 — Agent-Native Posts

  • Agent provenance fully rendered in the UI: [agent: claude-code ✓] badge on posts
  • devlog-agent archetype: posts daily commit summary for a repo
  • music-agent archetype: posts when a MIDI commit lands in a music repo
  • oracle-agent archetype: posts cross-repo code intelligence reports (hotspots, churn, blast radius)
  • All agent posts signed with derived Ed25519 sub-key, traceable to root identity

Phase 05 — MPay Integration

  • ReactDelta carries optional mpay: {from, to, amount_usdc, sig} payload
  • muse social react <post_id> ❤️ --tip 0.10 — react + send $0.10 USDC tip
  • Profile page shows cumulative tips received (MPay ledger aggregate)
  • Pay-to-boost: user-controlled local feed weighting based on tip amounts (optional, never platform-imposed)
  • AVAX address required on profile to receive tips

Phase 06 — Cross-Instance Federation

  • muse social follow gabriel --hub https://staging.musehub.ai — follow across instances
  • DID document resolution: /.well-known/did.json per handle on any musehub instance
  • muse social timeline merges local + all remote instance feeds seamlessly
  • MSign proofs verified against originating instance's key registry

Phase 07 — Encrypted DMs

  • Private branches: social/dm/<recipient_handle> — not pushed to public remotes
  • X25519 key exchange derived from sender + recipient Ed25519 keys (standard conversion)
  • Delta blobs encrypted before commit — server stores ciphertext, never plaintext
  • muse social dm gabriel "hey" — encrypt + commit to private DM branch
  • muse social dms — list DM threads; decrypt + render locally

Why Now

Every serious attempt at a decentralised social network has failed for the same reason: they built the social layer first and tried to add content-addressing and cryptographic identity as an afterthought. ActivityPub has identity problems. Nostr is close but lacks the commit DAG — there's no history, no merge, no branching. Farcaster is on-chain but the UX is hostile. Bluesky has the AT Protocol but it's centralised in practice.

Muse has the right primitives already. The object store is there. The DAG is there. The cryptographic identity is there. The wire protocol is there. The domain plugin interface is there. MuseHub profile pages are there. The only missing piece is a SocialDomainPlugin and CLI subcommands.

This isn't a new product. It's a 5-line domain_id = "social" declaration and eight phases of wiring.

The algo-free timeline isn't a differentiator. It's the default. Chronological order falls out of committed_at sort. The algorithm has to be actively built and imposed — without one, you just get the truth.


Success Metrics

  • muse social post "hello" commits a signed PostDelta to the social repo
  • muse social follow alice adds alice's feed as a remote
  • muse social timeline merges all followed feeds chronologically with zero duplicates
  • Every post in the timeline is verifiable with muse verify-commit <commit_id>
  • Real-time streaming: post lands in subscriber's timeline in < 500ms of push
  • MuseHub profile social tab renders timeline with correct provenance badges
  • Cross-instance follow works: muse social follow gabriel --hub https://staging.musehub.ai
  • Agent posts carry full provenance: agent_id, model_id, verified signature
  • MPay tip completes: ReactDelta + USDC transfer in one commit
  • Encrypted DM: only sender + recipient can decrypt; server stores ciphertext
Activity4
gabriel opened this issue 44 days ago
gabriel 44 days ago

✅ Phase 00 Complete — SocialPlugin Core

Commit: sha256:6296849dd5188cfda81467810 Branch: merged to dev on muse repo Tests: 43/43 GREEN (0 regressions in 176 related existing tests)

What landed

File Description
muse/plugins/social/plugin.py SocialPlugin — full 6-method MuseDomainPlugin protocol
muse/plugins/registry.py Registered under domain key "social"
tests/test_social_plugin.py 43 tests: shape, schema, in-memory ops, merge, docstrings

Key design decision made during TDD

Follows are stored as graph/follows/<handle>.json (one file per follow), not a single following.json array. This means set-union merge is automatic — following alice on one branch and bob on another has zero conflicts, identical to how posts work. No special JSON merge logic needed.

Merge rules implemented

  • posts/ — set-algebraic, independent. Same content hash = same post = no conflict. Reposts deduplicate naturally.
  • reactions/ — set-algebraic, independent. Concurrent reactions always merge cleanly.
  • graph/follows/ — set-algebraic, independent. Per-handle files, set-union on merge.
  • profile.json — non-independent. Conflict when both branches edit independently; last-writer-wins when only one side changed.

Up next: Phase 01 — muse social CLI surface (post, follow, timeline, react, reply, repost, unfollow, profile, drafts).

gabriel 44 days ago

✅ Phase 01 Complete — muse social CLI Surface

Commit: sha256:ad9018c59bfe0cae40106e518 Branch: merged to dev on muse repo Tests: 62/62 GREEN (148 passing across social + mist + registry suites — zero regressions)

What landed

File Description
muse/cli/commands/social.py Full muse social subcommand tree + pure state helpers
muse/cli/app.py social_cmd registered under "social"
tests/test_social_cli.py 62 tests: shape, state helpers, post, follow/unfollow, timeline, react, profile, docstrings

CLI surface shipped

muse social post "hello muse"                   # write posts/<post_id>.json
muse social post --reply-to <post_id> "reply"  # reply thread
muse social follow alice                         # graph/follows/alice.json
muse social unfollow alice                       # deletes the file
muse social timeline                             # sorted newest-first
muse social timeline --limit 20 --json          # structured output
muse social react <post_id> ❤️                  # reactions/<id>.json
muse social profile --set-bio "building..."    # update profile.json
muse social profile --json                      # read profile

Key design decisions

  • Posts are content-addressed on (body + reply_to + quote_of + created_at) — same inputs always produce the same post_id. Posting the same message twice is idempotent.
  • Reactions are content-addressed on (post_id + emoji) — reacting with ❤️ twice produces one file, not two.
  • Follows are per-handle files (graph/follows/<handle>.json) — set-union merge with no JSON parsing required. This design decision from Phase 00 pays off immediately.
  • All commands write state to the working tree. Stage and commit separately with muse code add . && muse commit --sign.

Up next: Phase 02 — Wire Protocol (SUBSCRIBE_SOCIAL frame, real-time streaming deltas, Harmony dedup)

gabriel 44 days ago

📝 Phase 02 — Design Correction

Original framing was wrong. MWP is transport — domain-agnostic by construction. Adding SUBSCRIBE_SOCIAL frames to MWP would couple domain semantics into the transport layer.

Corrected Phase 02: MuseHub Social API

The push path already works — social commits are just commits, MWP carries them exactly like code commits. Phase 02 is the server-side that makes social repos first-class on the hub:

Scope

Endpoint Description
GET /api/social/{handle} Paginated posts from a handle's social repo. Reads the social repo's snapshot manifest, loads post objects, returns sorted feed.
GET /api/social/{handle}/stream SSE stream — fires social_delta events when a social-domain push lands for {handle}. Real-time subscription without polling.
Push handler extension Detect domain="social" on push completion, extract post deltas from the commit, fan out to active SSE subscribers.

Why this is right

  • MWP stays transport-only. No domain semantics in the wire layer.
  • The feed endpoint is a pure read path over existing content-addressed objects.
  • The SSE fan-out reuses the same pattern as MCP streaming — no new infrastructure.
  • A UI (Phase 03) can be built on top of these two endpoints with no further backend changes.
gabriel 44 days ago

✅ Phase 02 Complete — MuseHub Social API

28/28 tests GREEN across 5 test classes.

What shipped

Three new endpoints in musehub/api/routes/musehub/social.py:

Endpoint Purpose
GET /api/social/{handle} Cursor-paginated posts feed — reads social-domain repo snapshot manifest, loads posts/*.json objects from the Muse store, returns sorted newest-first
GET /api/social/{handle}/stream SSE real-time subscription — delivers social_delta events when new social pushes land for a handle

New service layer in musehub/services/musehub_social.py:

  • get_social_feed(session, handle, *, limit, cursor) — full pagination with cursor support
  • subscribe_handle / unsubscribe_handle — in-process SSE subscriber registry (per-handle asyncio.Queue sets)
  • fan_out_to_subscribers(handle, event) — push events to all active SSE connections for a handle; no-op when no subscribers

Design notes

  • Social state lives entirely in the Muse object store — no new DB tables. Posts are posts/*.json content-addressed files in a domain_id="social" repo.
  • The subscriber registry is module-level in-process state; intentionally not distributed (single node). Horizontal scaling would need Redis pub/sub — not needed yet.
  • BaseHTTPMiddleware + SSE body streaming doesn't work with ASGI in-process test transport (well-known limitation). SSE tests verified via route registration check + service-level fan-out tests.

Next

Phase 03 — Social domain UI in musehub: profile feed page (/{handle}/social), real-time timeline view using the SSE stream, post composer.