Skip to content

feat(gateway): Phase 2 foundations — prompt sender_device param + advertised presence endpoints#131

Merged
OmarB97 merged 1 commit into
mainfrom
feat/gateway-remote-attach-foundations
Jun 10, 2026
Merged

feat(gateway): Phase 2 foundations — prompt sender_device param + advertised presence endpoints#131
OmarB97 merged 1 commit into
mainfrom
feat/gateway-remote-attach-foundations

Conversation

@OmarB97

@OmarB97 OmarB97 commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Why

Channels Phase 2 (cross-device attach, per docs/channels-design.md) needs two foundations before any remote client can exist: a protocol way for that client to declare which device a prompt was typed on (otherwise every message in a shared session gets attributed to the hosting gateway's device), and a dialable address in the presence records so a second device that discovers a live session knows where to connect. Both are inert for today's local-only clients and activate the moment a remote client ships.

What changed

  • tui_gateway/server.py: prompt.submit accepts an optional sender_device param, sanitized by a new _sanitize_sender_device (non-strings → empty, whitespace collapsed, 80-char cap — it renders as a chat label, so it is not trusted verbatim). Stashed on the session under history_lock, popped by _run_prompt_submit, handed to the agent per turn.
  • agent/conversation_loop.py: the turn's user-message dict carries the declared sender; the API-message builder strips sender_device exactly like reasoning/finish_reason/_thinking_prefill so strict providers (Mistral et al) never see it.
  • run_agent.py: the session-DB flush passes sender_device for user rows — explicit remote sender wins; absent stays None so append_message's local-device auto-stamp (schema v17) applies unchanged.
  • hermes_cli/web_server.py: new _presence_advertise_endpoint(host, port) + _primary_lan_ip(). Non-loopback dashboard binds advertise ws://<addr>:<port>/api/ws into HERMES_SESSION_PRESENCE_ENDPOINT via setdefault (operator-set env always wins). Wildcard binds resolve the primary outbound IPv4 via the UDP-connect trick (no packet sent, no DNS, no mesh tooling); loopback binds advertise nothing because the address is meaningless off-machine.
  • Tests: tests/cli/test_presence_advertise_endpoint.py (5 cases) + sanitizer cases in test_concurrent_attach.py.

How to review

  1. _sanitize_sender_device + the prompt.submit stash and _run_prompt_submit pop (one lock-scoped handoff).
  2. conversation_loop.py — the user-dict tag + the API strip; confirm the strip sits with its four siblings.
  3. run_agent.py flush — confirm None for non-user rows preserves assistant/tool NULL semantics.
  4. web_server.py — the two helpers and the setdefault at startup (placed right after app.state.bound_host/port).

Evidence

Verification

  • tests/cli/test_presence_advertise_endpoint.py + tests/tui_gateway/test_concurrent_attach.py + tests/tui_gateway/test_protocol.py + tests/test_sender_attribution.py — 79 passed.
  • py_compile clean on all four touched modules.

Risks / gaps

  • The full param path (remote ws client → prompt.submit → stored row) has no end-to-end test until a remote client exists — accepted scope, the three layers are unit-pinned and the integration work is the Phase 2 desktop client tracked in F-003-multi-participant-channels.
  • prompt.background does not yet accept sender_device — accepted scope, background prompts are gateway-originated (no remote typer) today.
  • Advertised endpoints assume the ws path /api/ws — low risk, it is the only mount point and the constant lives beside the helper.
  • Token/auth hardening for non-loopback binds stays as designed in docs/channels-design.md (the --insecure opt-in contract is unchanged here) — covered by the Phase 2 listen-auth item on F-003-multi-participant-channels.

Collaborators

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

…ertised presence endpoints

Channels Phase 2 groundwork (docs/channels-design.md): a client attached
from ANOTHER device needs (a) a way to declare who typed a prompt and (b)
a dialable address to find the hosting gateway in the first place.

- tui_gateway/server.py: prompt.submit accepts optional sender_device
  (sanitized: whitespace-collapsed, 80-char cap); stashed on the session
  under history_lock and handed to the conversation loop per turn.
- agent/conversation_loop.py: the turn's user message dict carries the
  declared sender; the API-message builder strips it for strict providers
  (same pattern as reasoning/finish_reason/_thinking_prefill).
- run_agent.py: the session-DB flush passes sender_device through for user
  rows — explicit remote sender wins, absent still auto-stamps the local
  device (schema v17 behavior).
- hermes_cli/web_server.py: when the dashboard binds beyond loopback, the
  session-presence records now advertise a dialable ws endpoint
  (HERMES_SESSION_PRESENCE_ENDPOINT via setdefault — operator config still
  wins; wildcard binds resolve the primary LAN IP via the UDP-connect
  trick; loopback binds advertise nothing). Zero-dep: works with no
  mesh/tailnet tooling; an explicit Tailscale/LAN bind advertises itself.
- tests: 5 endpoint-advertisement cases + sanitizer cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🔎 Lint report: feat/gateway-remote-attach-foundations 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: 10553 on HEAD, 10552 on base (🆕 +1)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 5543 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@OmarB97 OmarB97 merged commit 2c9e0cd into main Jun 10, 2026
16 of 22 checks passed
OmarB97 pushed a commit that referenced this pull request Jun 10, 2026
… fork consolidation; finish fork-feature ports

Per-cluster restoration with the test suite as the oracle, after comparing
the merged tree's failures against a pristine-upstream run in the same
environment (14 file-level deltas, now zero):

- gateway/run.py: upstream wholesale (fork's monolith had undone the mixin
  decomposition; both real fork deltas re-applied — voice_ack_callback
  **kwargs; the custom-providers context-length fix exists upstream).
- agent/conversation_loop.py + turn_context.py: upstream structure with the
  fork features regrafted at their new homes — sender_device attribution
  (#131), preflight token-usage emission + compression-complete status and
  live-estimate snapshots (#126).
- agent/chat_completion_helpers.py: upstream wholesale (brings the second
  partial-stream-stub routing site and the NousResearch#6600 cancellation fix).
- agent/tool_executor.py: usage= kwarg on tool start/complete callbacks now
  falls back to the bare 3-arg form for legacy receivers.
- tools/approval.py: upstream's resolved-HERMES_HOME rewrite + normalize
  steps restored alongside the fork's self-host kill guard (#128).
- hermes_cli/main.py: desktop install-identity stale-build cluster and the
  post-subcommand global-flag hoister ported from fork main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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