Skip to content

feat: guardian action expiry sweep, voice thread visibility, and settings#7536

Merged
noanflaherty merged 1 commit into
feature/voice-cross-guardianfrom
voice-cross-guardian/m7-settings-thread-expiry
Feb 24, 2026
Merged

feat: guardian action expiry sweep, voice thread visibility, and settings#7536
noanflaherty merged 1 commit into
feature/voice-cross-guardianfrom
voice-cross-guardian/m7-settings-thread-expiry

Conversation

@noanflaherty
Copy link
Copy Markdown
Contributor

@noanflaherty noanflaherty commented Feb 24, 2026

Summary

  • Guardian action expiry sweep: Adds a periodic 60-second sweep that detects expired cross-channel guardian action requests. Expired requests have their deliveries marked expired, pending questions expired, and expiry notices sent to external channels (Telegram, SMS) and mac guardian threads.
  • Voice thread visibility: Updates the session filter in ThreadSessionRestorer and ThreadManager to allow voice-channel threads (sourceChannel == "voice") to appear in the desktop conversation list alongside standard threads.
  • Voice settings card: Adds a "Voice (Phone Calls)" card to the Settings Connect tab showing Twilio credential and phone number readiness status for voice calls.

Changes

  • assistant/src/memory/guardian-action-store.ts — Added getExpiredGuardianActionRequests() and getDeliveriesByRequestId() query functions; imported lt from drizzle-orm
  • assistant/src/calls/guardian-action-sweep.ts — New file implementing the sweep timer with startGuardianActionSweep() / stopGuardianActionSweep()
  • assistant/src/runtime/http-server.ts — Wired up sweep start/stop alongside existing guardian expiry sweep
  • clients/macos/.../ThreadSessionRestorer.swift — Updated filter to pass through voice channel sessions
  • clients/macos/.../ThreadManager.swift — Updated filter to pass through voice channel sessions (pagination path)
  • clients/macos/.../SettingsConnectTab.swift — Added voice card with readiness indicators

Test plan

  • Verify guardian action requests expire correctly after their expiresAt timestamp
  • Verify expiry notices are sent to Telegram/SMS delivery destinations
  • Verify voice call threads appear in the desktop thread list
  • Verify Voice card shows correct status based on Twilio credential/number state

Generated with Claude Code


Open with Devin

…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>
@noanflaherty noanflaherty merged commit f4c6182 into feature/voice-cross-guardian Feb 24, 2026
@noanflaherty noanflaherty deleted the voice-cross-guardian/m7-settings-thread-expiry branch February 24, 2026 03:57
Copy link
Copy Markdown
Contributor

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

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

@@ -241,6 +241,38 @@ export function expireGuardianActionRequest(id: string): void {
.run();
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.

🟡 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)

Open in Devin Review

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

Copy link
Copy Markdown

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

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: 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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);
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 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 👍 / 👎.

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