[200 MRG] feat(presence): collaborative workspace presence (Fixes #30) - #89
Open
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Open
Conversation
) Peers announce themselves by writing one JSON record into a shared presence directory and refreshing it on a heartbeat; listing that directory is the whole discovery protocol. No server, no ports, no new dependency - the workspace root the collaborators already share is the default location. - src/lappa/presence.py: PresenceStore (join/heartbeat/leave/peers/reap), atomic os.replace writes, mtime-based freshness so unsynced clocks on a share cannot hide a live peer, same-host pid check so a crashed IDE drops out at once, tolerant reads (torn/foreign files skipped). - API: GET /api/presence, POST /api/presence/join|heartbeat|leave, GET /api/presence/file. PUT /api/files now reports who else has the file open (advisory - the save still happens) and never turns a read-only client into a peer. - CLI: lappa presence list|where|session|reap|dir. - docs/presence.md: design, record format, failure modes, and what is deliberately out of scope (no locking, no co-editing, lost-update protection still to come). - 41 tests (28 store + 13 API/CLI); suite 195 passed.
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.
Fixes #30
Acceptance for this bounty is Design + MVP, so this PR ships both:
docs/presence.md(the design) and a working presence layer behind the API, the CLI and tests.What Lappa does today
Two people pointed at the same workspace — a share, a NAS, a synced folder, or just two IDE processes on one machine — cannot see each other at all.
PUT /api/fileswrites the file; the second save silently replaces the first, and nothing anywhere hints that somebody else was in it. Presence is the smallest thing that makes the other person visible before that happens.The protocol
Every session writes one small JSON record into a shared presence directory and refreshes it on a heartbeat. Listing that directory is the whole discovery mechanism. No server, no ports, no database, no new dependency.
Directory resolution: explicit
--dir→LAPPA_PRESENCE_DIR→<first workspace root>/.lappa/presence→ app-data fallback. Defaulting to the workspace root is why this needs no infrastructure: if two people can open the same package, they can already write the same folder.Heartbeat 15 s, TTL 45 s — two missed beats still leave a peer online, so a slow share does not make the list blink.
The two parts that are not obvious
1. Freshness comes from mtime, not from the timestamp inside the record. The obvious version compares the peer's
seen_atagainst yournow. On a shared drive that is wrong: hosts drift, and a laptop whose clock is five minutes behind would look permanently offline to everyone — present, heartbeating, invisible. Both numbers now come from the same clock (the filesystem's), so drift cancels.seen_atis still surfaced asclock_skew_sso the UI can report a bad clock instead of hiding the person behind it.2. A crashed IDE leaves a fresh-looking record. Nothing deletes it, so it stays "online" until the TTL runs out. When a record claims our own host, the pid is checked directly and the dead session drops out at once. A pid from another host is never second-guessed — it means nothing in our process table — and a recycled pid can only make a dead peer look alive, which is the safe direction to be wrong in.
Records are written to a temp file and moved in with
os.replace, so a reader sees the whole old record or the whole new one, never half of one. Reads tolerate torn/empty/foreign files instead of throwing: a shared folder is not a database and eventually somebody drops aREADME.txtin it.Save conflicts
PUT /api/filesnow answers with whoever else has that file open:{"ok": true, "path": "launch/sim.launch.py", "conflicts": [{"user": "bob", "host": "bob-nuc", "session_id": "51bb…", "age_s": 3.1}]}Advisory only — the save always happens, presence never takes a lock, and clients that ignore the new field behave exactly as before.
GET /api/presence/file?path=…answers the same question without writing anything, so the IDE can show it the moment a file is opened.Two deliberate boundaries, both pinned by tests:
GET /api/presencenever creates a record (a status poll must not become a peer), and a save only refreshes a record for a session that already joined.Surface
GET /api/presence,POST /api/presence/join|heartbeat|leave,GET /api/presence/filelappa presence list | where | session | reap | dirdocs/presence.md— design, record format, failure-mode table, out-of-scopelappa presence session --hold 60joins, heartbeats and leaves on exit (including Ctrl-C) — two terminals are enough to demo multi-user presence on one machine without a share.Verified live, not only in unit tests
Real
lappa serveon :8899, a second process holding a file via the CLI, a third process listing:Then
kill -9on bob: his record file is still on disk, and he is gone from the live list within a second (dead pid, same host), still visible under--stale.leaveremoves the file.Tests
41 new (
tests/test_presence.py28,tests/test_presence_api.py13); full suite 195 passed, 3 skipped (154 before). No existing test touched. Among them: a peer 600 s off-clock stays online with the skew reported; a dead pid on this host is offline while the same dead pid on another host is trusted; torn/empty/list-shaped/foreign files are skipped;../../evilas a session id is refused rather than sanitised;reapkeeps recently-stale records but clears long-abandoned ones; a broken presence directory returns 503 onjoinand still lets the file save succeed with200.Mutations, both directions — freshness back to the writer's clock → 6 tests fail; pid check removed → 1;
touch()auto-joining → 1;reapdeleting anything stale → 1;peers_on_filecounting yourself → 1. All reverted, 195 green.ruff check src tests: the three new files are clean. The five findings insidecli.pyareB008ontyper.Optiondefaults — the same pattern every existing command in that file uses (11 pre-existing findings there).Deliberately out of scope
Not a lock, not co-editing (no OT/CRDT), and no lost-update protection yet:
PUT /api/filesis still last-write-wins, presence only makes the collision visible. The natural next step is optimistic concurrency on the write endpoint (send the mtime/hash you loaded, get a409if it moved) — that is a change to the file API rather than to presence, and is better reviewed on its own than half-done here.Claim ritual done: starred Lappa / mergeos / mergeos-contracts,
I claim this bountyon #30, and the claim comment with this issue link on mergeos#1.