Skip to content

fix: address round-2 voice-cross-guardian review feedback#7552

Merged
noanflaherty merged 1 commit into
feature/voice-cross-guardianfrom
voice-cross-guardian/fix-review-feedback-2
Feb 24, 2026
Merged

fix: address round-2 voice-cross-guardian review feedback#7552
noanflaherty merged 1 commit into
feature/voice-cross-guardianfrom
voice-cross-guardian/fix-review-feedback-2

Conversation

@noanflaherty

@noanflaherty noanflaherty commented Feb 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses the second round of review feedback on PR #7539:

  • P1 — Mac guardian-answer ordering (session-process.ts): Call answerCall() before resolveGuardianActionRequest() so that a failed delivery (e.g. call timed out) leaves the request pending for retry from another channel. Mirrors the pattern already fixed in channel-routes.ts.

  • P1 — Voice transcript/completion persistence (relay-server.ts, call-orchestrator.ts): Persist caller and assistant transcripts directly to conversation_store alongside the notifier fires. This ensures the voice thread retains transcript history even when no live daemon Session is listening on it.

  • Yellow — SKILL.md codeLength default (SKILL.md): Fix documented default from 4 to 6 to match the actual schema default.

Test plan

  • Verify mac-channel guardian answer flow calls answerCall before resolving the request
  • Verify voice transcripts persist to the voice conversation thread independently of Session callbacks
  • Verify SKILL.md documents codeLength default as 6

🤖 Generated with Claude Code


Open with Devin

1. Fix mac channel guardian-answer ordering: call answerCall before
   resolveGuardianActionRequest so failed delivery leaves request
   pending for retry from another channel (mirrors channel-routes.ts).

2. Persist voice transcripts directly to conversation_store alongside
   notifier fires so transcript history survives without a live
   daemon Session listening on the voice thread.

3. Fix SKILL.md codeLength default documentation (4 → 6) to match
   the actual schema default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@noanflaherty noanflaherty merged commit eb63e8d into feature/voice-cross-guardian Feb 24, 2026
1 check failed
@noanflaherty noanflaherty deleted the voice-cross-guardian/fix-review-feedback-2 branch February 24, 2026 04:28

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 2 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +459 to +463
conversationStore.addMessage(
session.conversationId,
'user',
JSON.stringify([{ type: 'text', text: msg.voicePrompt }]),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Duplicate transcript persistence when a live daemon Session is listening on the voice conversation

Every caller and assistant transcript is persisted twice to the voice conversation when a daemon Session is actively listening. The new direct conversationStore.addMessage calls write the raw transcript, and then fireCallTranscriptNotifier triggers the notifier callback in session-notifiers.ts:125-129 which also calls conversationStore.addMessage on the same conversationId.

Root Cause and Impact

The new code at assistant/src/calls/relay-server.ts:459-462 persists caller transcripts:

conversationStore.addMessage(
  session.conversationId, // voice conversation
  'user',
  JSON.stringify([{ type: 'text', text: msg.voicePrompt }]),
);

Then fireCallTranscriptNotifier on line 464 fires the callback registered in assistant/src/daemon/session-notifiers.ts:119-129, which also persists to the same conversation:

conversationStore.addMessage(
  conversationId, // same voice conversation
  'assistant',
  JSON.stringify([{ type: 'text', text: transcriptText }]),
);

The same duplication occurs for assistant transcripts in call-orchestrator.ts:458-462 followed by fireCallTranscriptNotifier on line 463.

When no Session is listening the notifier doesn't fire, so only the new direct write happens (correct). But when a Session IS listening, both writes execute, producing two messages per transcript event. The messages also have inconsistent roles: the direct write uses 'user' for caller utterances while the notifier always writes as 'assistant' with a formatted prefix. This corrupts the voice conversation history with duplicate, inconsistently-roled messages.

Prompt for agents
The fix needs to coordinate between the new direct conversationStore.addMessage calls (in relay-server.ts:459-462 and call-orchestrator.ts:458-462) and the existing notifier callback in session-notifiers.ts:119-142 that also calls conversationStore.addMessage on the same conversation. Two approaches:

1. Remove the conversationStore.addMessage call from the notifier callback in assistant/src/daemon/session-notifiers.ts lines 125-129 (the registerCallTranscriptNotifier callback), since the new direct writes in relay-server.ts and call-orchestrator.ts now handle persistence unconditionally. The notifier would then only push to ctx.messages and send to the client. You'd also need to ensure the notifier's ctx.messages.push uses the same format/role as the direct write.

2. Alternatively, only call conversationStore.addMessage in relay-server.ts/call-orchestrator.ts when no notifier is registered (i.e., no live Session is listening). This could be done by checking if fireCallTranscriptNotifier returned without invoking a callback, but the current API doesn't support that.

Approach 1 is cleaner: keep the direct writes as the single source of truth for persistence, and let the notifier only handle in-memory state and client notifications.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +458 to +462
conversationStore.addMessage(
session.conversationId,
'assistant',
JSON.stringify([{ type: 'text', text: spokenText }]),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 Duplicate assistant transcript persistence when a live daemon Session is listening on the voice conversation

Same duplication issue as the caller transcript path, but for assistant transcripts in the call orchestrator.

Root Cause

The new code at assistant/src/calls/call-orchestrator.ts:458-462 persists the assistant transcript directly:

conversationStore.addMessage(
  session.conversationId,
  'assistant',
  JSON.stringify([{ type: 'text', text: spokenText }]),
);

Then on line 463, fireCallTranscriptNotifier triggers the notifier in assistant/src/daemon/session-notifiers.ts:125-129 which persists a second formatted copy to the same conversation as 'assistant' role with "**Live call transcript**\nAssistant: ..." prefix. This results in two assistant messages per LLM turn in the voice conversation when a Session is listening.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@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: 6bf00b628e

ℹ️ 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 +459 to 463
session.conversationId,
'assistant',
JSON.stringify([{ type: 'text', text: spokenText }]),
);
fireCallTranscriptNotifier(session.conversationId, this.callSessionId, 'assistant', spokenText);

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 Avoid double-persisting voice transcript messages

This block now writes the assistant transcript directly to conversation_store and then still calls fireCallTranscriptNotifier; when a daemon session is attached, the notifier callback in assistant/src/daemon/session-notifiers.ts also calls conversationStore.addMessage for the same transcript event. The same pattern was added for caller transcripts in assistant/src/calls/relay-server.ts, so any open voice thread now stores each utterance twice, inflating history/memory indexing and polluting later model context with duplicates.

Useful? React with 👍 / 👎.

noanflaherty added a commit that referenced this pull request Feb 24, 2026
#7539)

* refactor: rename ASK_USER marker to ASK_GUARDIAN (#7507)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: add voice channel identity and per-call voice conversations (#7512)

Co-authored-by: Claude <noreply@anthropic.com>

* fix: address M2 review feedback — call session lookup + voice probe (#7524)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: voice event projection, pointer messages, and bridge removal (#7529)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: DTMF callee verification for outbound voice calls (#7533)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: cross-channel guardian data model, store, and dispatch (#7534)

Co-authored-by: Claude <noreply@anthropic.com>

* feat: cross-channel guardian answer resolution (#7535)

When a guardian action request is dispatched to telegram/sms/mac channels
during a voice call, replies on any of those channels are now intercepted,
validated, and used to resume the call:

- Channel inbound (telegram/sms): intercept guardian answers early in
  handleChannelInbound(), with identity verification, single/multi-delivery
  disambiguation via request codes, and first-writer-wins resolution
- Mac thread: intercept in session-process processMessage() before the
  agent loop, routing the user message as a guardian answer
- Guardian dispatch: create mac conversations server-side with
  getOrCreateConversation() and seed them with the question text
- Store: add getPendingDeliveryByConversation() for mac channel routing

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* feat: guardian action expiry sweep, voice thread visibility, and voice settings card (#7536)

Add periodic sweep (60s interval) for expired cross-channel guardian action
requests. When a request expires: marks request+deliveries as expired, expires
pending questions, and sends expiry notices to external channels and mac threads.

Allow voice-channel threads to appear in the desktop thread list by updating
the session filter in both ThreadSessionRestorer and ThreadManager to pass
through sessions with sourceChannel == "voice".

Add a Voice (Phone Calls) card to the Settings Connect tab showing Twilio
credential and phone number readiness for voice calls.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update SKILL.md and ARCHITECTURE.md for voice-cross-guardian M1-M7 changes (#7538)

Reflect the cross-channel guardian architecture in documentation:
- SKILL.md: add DTMF callee verification section, update answering
  questions to describe ASK_GUARDIAN cross-channel dispatch with
  first-response-wins semantics, note mid-call steering via desktop
  chat is no longer supported, add accepted regressions section
- ARCHITECTURE.md: update outgoing calls intro to describe voice as
  first-class channel with per-call conversations, replace bridge-based
  Mermaid diagram flow with guardian dispatch flow, replace call-bridge
  key component with guardian-dispatch/guardian-action-store/guardian-
  action-sweep, replace Call Bridge section with Cross-Channel Guardian
  Consultation, add guardian_action_requests and guardian_action_deliveries
  SQLite tables, add guardian modules to Channel Guardian Security table

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address voice-cross-guardian review feedback (5 issues) (#7550)

1. Pass broadcast/assistantId to CallOrchestrator from RelayConnection via
   module-level setRelayBroadcast wired in lifecycle.ts, so mac desktop
   receives guardian_request_thread_created IPC events and multi-assistant
   deployments use the correct assistant ID.

2. Thread bearer token through guardian dispatch deliverToExternalChannel
   so gateway /deliver/{channel} calls include Authorization header.

3. Swap resolve/answerCall ordering in channel-routes guardian answer
   interception: call answerCall first, resolve only on success, so
   failed answers leave the request pending for retry.

4. Use content block array format for addMessage calls in
   guardian-dispatch.ts and guardian-action-sweep.ts to match codebase
   convention (JSON.stringify([{type:'text',text:'...'}])).

5. Expire deliveries in 'sent' status (not just 'pending') in
   expireGuardianActionRequest using inArray.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address round-2 voice-cross-guardian review feedback (#7552)

1. Fix mac channel guardian-answer ordering: call answerCall before
   resolveGuardianActionRequest so failed delivery leaves request
   pending for retry from another channel (mirrors channel-routes.ts).

2. Persist voice transcripts directly to conversation_store alongside
   notifier fires so transcript history survives without a live
   daemon Session listening on the voice thread.

3. Fix SKILL.md codeLength default documentation (4 → 6) to match
   the actual schema default.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make voice transcript/completion persistence session-independent

* fix voice call transcript handling and close review gaps

---------

Co-authored-by: Claude <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