feat: guardian action expiry sweep, voice thread visibility, and settings#7536
Conversation
…e settings card 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>
| @@ -241,6 +241,38 @@ export function expireGuardianActionRequest(id: string): void { | |||
| .run(); | |||
There was a problem hiding this comment.
🟡 expireGuardianActionRequest fails to expire deliveries with status 'sent', leaving them in inconsistent state
When the sweep expires a guardian action request, expireGuardianActionRequest() only updates deliveries where status = 'pending' to 'expired'. Deliveries that have already been successfully dispatched (status 'sent') — which are the ones that actually reached external channels like Telegram/SMS — are NOT marked as expired.
Root Cause and Impact
The expireGuardianActionRequest function at assistant/src/memory/guardian-action-store.ts:233-241 filters on eq(guardianActionDeliveries.status, 'pending'), so 'sent' deliveries are skipped. Compare this with resolveGuardianActionRequest at line 208-211 which marks ALL deliveries as 'answered' without a status filter — showing the intended behavior is to update all deliveries.
After the sweep runs:
- The request is correctly set to
'expired' 'pending'deliveries are correctly set to'expired''sent'deliveries remain in'sent'status — orphaned and inconsistent
The sweep comment at guardian-action-sweep.ts:41 says "Expire the request and all deliveries" but this doesn't happen for 'sent' deliveries. While current queries that look for 'sent' deliveries also check request.status = 'pending' (preventing functional issues today), the data inconsistency could cause problems if future queries rely on delivery status alone.
(Refers to lines 233-241)
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: b5f4f1930b
ℹ️ 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".
| expireGuardianActionRequest(request.id); | ||
|
|
||
| // Expire associated pending questions | ||
| expirePendingQuestions(request.callSessionId); |
There was a problem hiding this comment.
Expire only the request’s own pending question
Calling expirePendingQuestions(request.callSessionId) here expires all pending questions for the call, not just the one tied to this expired guardian action. If request A times out, the call continues, and request B is asked before the sweep runs, sweeping A will also expire B and valid guardian replies to B will fail with no pending question. The sweep should only expire request.pendingQuestionId (or otherwise scope expiration to this request).
Useful? React with 👍 / 👎.
| const deliveries = getDeliveriesByRequestId(request.id); | ||
|
|
||
| // Expire the request and all deliveries | ||
| expireGuardianActionRequest(request.id); |
There was a problem hiding this comment.
Guard notifications on successful expiry transition
This sweep sends expiry side effects unconditionally after calling expireGuardianActionRequest, but that update is conditional on the request still being pending. If a guardian reply resolves the request between the initial getExpiredGuardianActionRequests() read and this update, the update becomes a no-op and the code still emits “expired without response” notices, producing incorrect user-facing messages. Check whether the status transition actually occurred before expiring questions and notifying channels.
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>
Summary
ThreadSessionRestorerandThreadManagerto allow voice-channel threads (sourceChannel == "voice") to appear in the desktop conversation list alongside standard threads.Changes
assistant/src/memory/guardian-action-store.ts— AddedgetExpiredGuardianActionRequests()andgetDeliveriesByRequestId()query functions; importedltfrom drizzle-ormassistant/src/calls/guardian-action-sweep.ts— New file implementing the sweep timer withstartGuardianActionSweep()/stopGuardianActionSweep()assistant/src/runtime/http-server.ts— Wired up sweep start/stop alongside existing guardian expiry sweepclients/macos/.../ThreadSessionRestorer.swift— Updated filter to pass through voice channel sessionsclients/macos/.../ThreadManager.swift— Updated filter to pass through voice channel sessions (pagination path)clients/macos/.../SettingsConnectTab.swift— Added voice card with readiness indicatorsTest plan
expiresAttimestampGenerated with Claude Code