feat(state): messages.sender_device — device-level sender attribution (F-003 slice 1)#129
Merged
Merged
Conversation
…(F-003) First building block of multi-participant channel sessions: every user message records WHICH device it was typed on, so a session shared across devices reads like a group chat instead of an undifferentiated 1:1 thread. - hermes_state.py: schema v17 adds messages.sender_device (nullable; existing rows stay NULL — unknown origin is honest). append_message gains sender_device=None and auto-stamps user rows with the local resolved device name (config → MeshBoard → Tailscale → hostname), so every existing writer participates without call-site changes; remote writers (future fan-out) pass theirs explicitly. Assistant/tool rows stay NULL — the agent is not a sender. replace_messages preserves attribution across /retry//undo//compress transcript rewrites. - Read path is free: get_messages SELECT * + the REST messages endpoint return the column verbatim. - apps/desktop: SessionMessage/ChatMessage carry sender_device → senderDevice; toRuntimeMessage threads it through metadata.custom (same route as attachmentRefs); user bubbles render a small muted device label above the text. - tests: 7 new (auto-stamp, explicit override, assistant/tool NULL, resolution-failure degrade, v16→v17 migration, rewrite preservation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🔎 Lint report:
|
| Rule | Count |
|---|---|
unresolved-attribute |
3 |
unresolved-import |
1 |
First entries
tests/test_sender_attribution.py:69: [unresolved-attribute] unresolved-attribute: Attribute `executescript` is not defined on `None` in union `Connection | None`
tests/test_sender_attribution.py:86: [unresolved-attribute] unresolved-attribute: Attribute `execute` is not defined on `None` in union `Connection | None`
tests/test_sender_attribution.py:76: [unresolved-attribute] unresolved-attribute: Attribute `commit` is not defined on `None` in union `Connection | None`
tests/test_sender_attribution.py:8: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
✅ Fixed issues: none
Unchanged: 5537 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
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.
Why
Feature
F-003-multi-participant-channels(mesh backlog, blocked-on-dispatcher since 2026-06-08; taken inline as operator-direct work): the operator's product direction turns Hermes sessions into Discord-like channels synchronized across devices. The schema currently cannot express WHO sent a user message —messageshas role/content but no sender identity, so a session viewed from a second device reads as an undifferentiated 1:1 thread. This PR lands the F-003 slice: sender attribution with device-nickname identity (user accounts stay out of scope per the feature record).What changed
hermes_state.py: schema v17 —ALTER TABLE messages ADD COLUMN sender_device TEXT(migration replay-safe; existing rows stay NULL).append_message(sender_device=None)auto-stamps user rows with the local resolved device name via the existing 4-tier resolver; explicit values win (future fan-out writers pass their own). Assistant/tool rows stay NULL.replace_messages(the/retry//undo//compressrewrite path) round-trips the column so history rewrites don't erase attribution.get_messagesisSELECT *and/api/sessions/{id}/messagesreturns rows verbatim.apps/desktop:SessionMessage.sender_device→ChatMessage.senderDeviceintoChatMessages; threaded through assistant-uimetadata.custom(the existingattachmentRefsroute) intoRuntimeMessage; user bubbles render a small muted device label above the message text.tests/test_sender_attribution.py: 7 cases — auto-stamp, explicit override, assistant/tool NULL, resolver-failure degrade-to-NULL, v16→v17 migration, rewrite preservation.How to review
hermes_state.py— the v17 migration block, theappend_messagestamp (note therole == "user"guard), and thereplace_messagespassthrough.tests/test_sender_attribution.pydocuments every behavior decision.chat-messages.ts(mapping) →chat-runtime.ts(metadata.custom) →thread.tsx(label render), in that order.Evidence
test_user_message_auto_stamps_local_device/test_assistant_and_tool_rows_stay_nullpin the stamp semantics;test_replace_messages_preserves_attributionpins the rewrite path that would otherwise silently wipe the column.Verification
tests/test_sender_attribution.py— 7 passed.tests/test_hermes_state.pyfull suite — 264 passed, 0 failed.tsc -b0 errors; vitestsrc/lib+ stream-hook suites — 154 passed.Risks / gaps
F-003-multi-participant-channelsfollow-on (multi-client attach).sessions.source-as-origin rename and presence participant tracking from the F-003 record are deferred to the fan-out slice — tracked onF-003-multi-participant-channels.Collaborators