M5: Cross-channel guardian data model + dispatch#7534
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
| db.update(guardianActionDeliveries) | ||
| .set({ status: 'expired', updatedAt: now }) | ||
| .where( | ||
| and( | ||
| eq(guardianActionDeliveries.requestId, id), | ||
| eq(guardianActionDeliveries.status, 'pending'), | ||
| ), | ||
| ) | ||
| .run(); |
There was a problem hiding this comment.
🔴 expireGuardianActionRequest and cancelGuardianActionRequest skip 'sent' deliveries
When a guardian action request expires or is cancelled, the delivery status update only targets deliveries with status = 'pending', but in practice deliveries are immediately transitioned to 'sent' during dispatch. This leaves sent deliveries permanently stuck in 'sent' status instead of being properly expired/cancelled.
Root Cause and Impact
In assistant/src/calls/guardian-dispatch.ts:127, the mac delivery is immediately set to 'sent' via updateDeliveryStatus(delivery.id, 'sent'). External channel deliveries are also set to 'sent' on success at assistant/src/calls/guardian-dispatch.ts:164.
However, expireGuardianActionRequest at assistant/src/memory/guardian-action-store.ts:233-241 filters deliveries by eq(guardianActionDeliveries.status, 'pending'), and cancelGuardianActionRequest at assistant/src/memory/guardian-action-store.ts:261-269 does the same. Since deliveries transition from 'pending' to 'sent' almost immediately after creation, these expire/cancel operations will match zero delivery rows in practice.
Notably, resolveGuardianActionRequest at line 208-211 correctly handles this by updating ALL deliveries for the request regardless of their current status:
db.update(guardianActionDeliveries)
.set({ status: 'answered', respondedAt: now, updatedAt: now })
.where(eq(guardianActionDeliveries.requestId, id))
.run();The expire and cancel functions should use a similar approach — either updating all deliveries for the request, or at minimum also including 'sent' in the status filter.
Impact: Deliveries remain in 'sent' status after their parent request has been expired or cancelled, creating a data integrity inconsistency. Any admin/monitoring queries or future code that inspects delivery status without joining to the parent request will see stale data.
Prompt for agents
In assistant/src/memory/guardian-action-store.ts, the expireGuardianActionRequest function (lines 233-241) and cancelGuardianActionRequest function (lines 261-269) both filter deliveries by status = 'pending', but deliveries are almost always in 'sent' status by the time expire/cancel runs. Fix both functions to also include 'sent' deliveries in the status filter, or remove the status filter entirely (matching the pattern used in resolveGuardianActionRequest at lines 208-211 which updates all deliveries regardless of status). For example, use drizzle-orm's `inArray` or `or()` to match both 'pending' and 'sent' statuses, or simply filter only by requestId like resolveGuardianActionRequest does.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 506fab2266
ℹ️ 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".
| this.broadcast = opts?.broadcast; | ||
| this.assistantId = opts?.assistantId ?? 'self'; |
There was a problem hiding this comment.
Pass session assistant ID into guardian dispatch
This constructor now defaults assistantId to 'self' when no options are provided, but the production call path still instantiates CallOrchestrator without opts (assistant/src/calls/relay-server.ts). For calls initiated under any non-default assistant, dispatchGuardianQuestion will look up guardian bindings under the wrong assistant ID and skip configured Telegram/SMS destinations, so cross-channel guardian delivery silently fails in multi-assistant setups.
Useful? React with 👍 / 👎.
| title: `Guardian question: ${pendingQuestion.questionText.slice(0, 80)}`, | ||
| } as ServerMessage); | ||
| } | ||
| updateDeliveryStatus(delivery.id, 'sent'); |
There was a problem hiding this comment.
Avoid marking mac delivery sent when nothing was emitted
The mac delivery is marked sent unconditionally even though event emission is guarded by if (broadcast), so we record success when no IPC message was actually published. Given the current orchestrator construction path does not provide a broadcast callback, this causes guardian mac notifications to be dropped while the delivery row is still marked successful, preventing reliable retry/recovery.
Useful? React with 👍 / 👎.
#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>
Part of #7488. Adds guardian_action_requests and guardian_action_deliveries tables, store layer with first-response-wins resolution, dispatch engine in call orchestrator for ASK_GUARDIAN, and guardian_request_thread_created IPC event.