Skip to content

feat(s2s): route conversations to /v1/voice/conversation (extraction parity) - #40

Merged
WingedGuardian merged 1 commit into
mainfrom
feat/s2s-conversation-route
Jul 19, 2026
Merged

feat(s2s): route conversations to /v1/voice/conversation (extraction parity)#40
WingedGuardian merged 1 commit into
mainfrom
feat/s2s-conversation-route

Conversation

@WingedGuardian

Copy link
Copy Markdown
Owner

Summary

Switches the s2s bridge from the legacy one-blob POST /api/t/memory_store landing to the core cumulative-turns route POST /v1/voice/conversation (added on the GENesis-AGI side, W0.5), so voice conversations feed the memory extraction pipeline like every other channel instead of landing as an ever-growing raw blob.

Background

On every client disconnect the bridge POSTed its full cached context as one "Voice conversation [...]" blob. The context grows monotonically for the whole process lifetime, and the disconnect can fire 2–3× per teardown, so the core accumulated ~120 duplicated, ever-growing episodic blobs (each embedded in the vector store) that were never mined for facts.

Change

  • GenesisToolService.store_conversationsync_conversation: filters the cached context to user/assistant turns and POSTs the cumulative list to /v1/voice/conversation. The core route appends only turns beyond the transcript's line count, so replays / double-fires / cumulative re-sends land exactly once.
  • Session scoping (gap-split): the bridge context never resets within a process, so a single id would yield one transcript spanning weeks. The session id rotates when a persist arrives more than SESSION_REUSE_TIMEOUT_SECONDS after the previous one (the same window SessionManager uses to decide a conversation ended), and pre-gap turns are based out — so transcripts map ~1:1 to human conversations. A defensive guard also rotates if the cumulative list ever shrinks below the session base (never observed; the context is monotonic in prod).
  • 4xx is permanent: a validation / cap / auth rejection is saved to the local fallback file immediately (not retried); 5xx / network errors keep the 3-attempt retry. Bearer auth (GENESIS_TOKEN, the same token the bridge already uses for /v1/voice/tool_call), the log-before-POST durability line, and the fallback-on-failure path are unchanged. No new config.

Testing

  • 14 rewritten unit tests: endpoint + payload shape, turn filtering (developer / tool / non-str dropped), gap-split rotation + base slicing, shrink guard, empty-slice churn skips the POST, 4xx-no-retry, 5xx-retry, fallback contents, restart independence.
  • Full s2s suite (85 tests) green on the deployed edge environment (pipecat 1.3.0).
  • Cross-repo seam drill: real bridge payloads fed into the core sync_cumulative + run_extraction_cycle — a double-fire lands 0, a gap opens a new transcript, both are mined and watermarked.

Deploy ordering

Land + deploy the GENesis-AGI side (the W0.5 route plus its sync_cumulative concurrency fix) on the core first; verify the core serves 401 (not 404/405) on POST /v1/voice/conversation, then redeploy the bridge and restart the service. Until the bridge redeploys, the core's daily voice-hygiene sweep keeps clearing any interim blobs.

🤖 Generated with Claude Code

…parity)

The s2s bridge persisted each voice conversation as a single growing
"Voice conversation [...]" blob to POST /api/t/memory_store on every
disconnect — bypassing the core memory extraction pipeline and re-writing a
cumulative blob per disconnect (weeks of duplicates on the primary).

Switch to the core cumulative-turns route added in GENesis-AGI PR-B:
sync_conversation filters the bridge's cached context to user/assistant turns
and POSTs the cumulative list to POST /v1/voice/conversation. The core route
(sync_cumulative) appends only turns beyond the transcript's line count, so
replays / double-fires / cumulative re-sends land exactly once and each
conversation is mined by the extraction job like every other channel.

Session scoping: the bridge context grows monotonically for the whole process
lifetime, so a single id would produce one transcript spanning weeks. The
session id ROTATES when a persist arrives more than SESSION_REUSE_TIMEOUT_SECONDS
after the previous one (the same window the SessionManager uses to decide a
conversation ended) and turns before the gap are based out — so transcripts map
~1:1 to human conversations. A defensive guard also rotates if the cumulative
list ever shrinks below the session base (never observed; the context is
monotonic in prod).

4xx responses are now treated as permanent (saved to the local fallback file
immediately, not retried); 5xx / network errors keep the 3-attempt retry. Bearer
auth, the log-before-POST durability line, and the fallback-on-failure path are
unchanged. No new config — GENESIS_TOKEN already authenticates the same
_check_voice_token the edge passes for /v1/voice/tool_call today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4e9ad15461

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +121 to +125
gap = self._session_id is not None and now - self._last_persist_ts > self._session_split_seconds
if self._session_id is None or gap:
# New conversation: turns accumulated so far belong to prior epochs,
# so base them out (0 on the very first persist).
self._start_session(base=self._last_turn_count if gap else 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope transcript sessions per satellite

When two different client_id/satellite_id values disconnect within SESSION_REUSE_TIMEOUT, this reuses the single _session_id, _last_turn_count, and _session_base from the previous satellite because the state is global to one GenesisToolService. main.py passes each disconnect through the same service with client_id as satellite_id, so the second device can post under the first device's cumulative session; with line-count dedupe on that session, a shorter second transcript may append nothing or get merged into the wrong conversation. Keep the session/base/last-persist state keyed by satellite_id.

Useful? React with 👍 / 👎.

@WingedGuardian
WingedGuardian merged commit de59c59 into main Jul 19, 2026
4 checks passed
@WingedGuardian
WingedGuardian deleted the feat/s2s-conversation-route branch July 19, 2026 23:28
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