Skip to content

feat(state): messages.sender_device — device-level sender attribution (F-003 slice 1)#129

Merged
OmarB97 merged 1 commit into
mainfrom
feat/message-sender-attribution
Jun 9, 2026
Merged

feat(state): messages.sender_device — device-level sender attribution (F-003 slice 1)#129
OmarB97 merged 1 commit into
mainfrom
feat/message-sender-attribution

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jun 9, 2026

Copy link
Copy Markdown
Owner

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 — messages has 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//compress rewrite path) round-trips the column so history rewrites don't erase attribution.
  • Read path: zero changes needed — get_messages is SELECT * and /api/sessions/{id}/messages returns rows verbatim.
  • apps/desktop: SessionMessage.sender_deviceChatMessage.senderDevice in toChatMessages; threaded through assistant-ui metadata.custom (the existing attachmentRefs route) in toRuntimeMessage; 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

  1. hermes_state.py — the v17 migration block, the append_message stamp (note the role == "user" guard), and the replace_messages passthrough.
  2. tests/test_sender_attribution.py documents every behavior decision.
  3. Desktop: 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_null pin the stamp semantics; test_replace_messages_preserves_attribution pins the rewrite path that would otherwise silently wipe the column.

Verification

  • tests/test_sender_attribution.py — 7 passed.
  • tests/test_hermes_state.py full suite — 264 passed, 0 failed.
  • Desktop: tsc -b 0 errors; vitest src/lib + stream-hook suites — 154 passed.

Risks / gaps

  • Live mid-session attribution display only updates on hydration (stored messages); streaming your own just-typed message shows no label until reload — accepted scope, the typing device is implicit to the typer and fan-out display is the F-003-multi-participant-channels follow-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 on F-003-multi-participant-channels.
  • Device-name resolution at append time adds one cached lookup per user message (process-lifetime cache) — low risk.

Collaborators

  • @OmarB97 (operator)
  • Claude Fable 5 (Claude Code)

…(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>
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

🔎 Lint report: feat/message-sender-attribution vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10550 on HEAD, 10543 on base (🆕 +7)

🆕 New issues (4):

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.

@OmarB97 OmarB97 merged commit 461c1a9 into main Jun 9, 2026
17 of 23 checks passed
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.

1 participant