Files
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

664 lines
38 KiB
Go

package diagnostics
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/identity"
"data-terminal-cloud/internal/outbox"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrInvalid = errors.New("diagnostic request is invalid")
ErrAccessDenied = errors.New("diagnostic access is denied")
ErrNotFound = errors.New("diagnostic resource was not found")
ErrStateConflict = errors.New("diagnostic action conflicts with current state")
requestIDPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:/-]{2,127}$`)
searchFieldAllowed = map[string]bool{"device_id": true, "conversation_id": true, "room_id": true, "provider_task_ref": true, "request_id": true}
retryableJobTypeAllowed = map[string]bool{
"platform.noop": true,
"conversation.room_issued": true,
"conversation.agent_start_requested": true,
"conversation.agent_start_reconcile_requested": true,
"conversation.agent_stop_requested": true,
"conversation.agent_stop_reconcile_requested": true,
"conversation.content_extraction_requested": true,
"call.story_extraction_requested": true,
"memory.index_requested": true,
"memory.delete_requested": true,
"privacy.access_requested": true,
"privacy.delete_requested": true,
"export.generate_requested": true,
"media.verify_requested": true,
"alert.webhook_test_requested": true,
}
)
type Record struct {
DeviceID string `json:"device_id"`
DeviceStatus string `json:"device_status"`
DeviceOnline bool `json:"device_online"`
FirmwareVersion string `json:"firmware_version,omitempty"`
DesiredConfigRevision int64 `json:"desired_config_revision"`
ReportedConfigRevision *int64 `json:"reported_config_revision,omitempty"`
LastDeviceErrorCode string `json:"last_device_error_code,omitempty"`
LastDeviceErrorAt *time.Time `json:"last_device_error_at,omitempty"`
ConversationID string `json:"conversation_id,omitempty"`
ConversationStatus string `json:"conversation_status,omitempty"`
ConversationFailureCode string `json:"conversation_failure_code,omitempty"`
AllocationExpiresAt *time.Time `json:"allocation_expires_at,omitempty"`
CredentialIssuedAt *time.Time `json:"credential_issued_at,omitempty"`
CredentialExpiresAt *time.Time `json:"credential_expires_at,omitempty"`
RoomID string `json:"room_id,omitempty"`
Provider string `json:"provider,omitempty"`
ProviderTaskRef string `json:"provider_task_ref,omitempty"`
ProviderTaskStatus string `json:"provider_task_status,omitempty"`
ProviderStartAttempts int `json:"provider_start_attempts"`
ProviderStopAttempts int `json:"provider_stop_attempts"`
ProviderNotificationCount int64 `json:"provider_notification_count"`
LastProviderEvent string `json:"last_provider_event,omitempty"`
LastProviderEventStatus string `json:"last_provider_event_status,omitempty"`
LastProviderEventAt *time.Time `json:"last_provider_event_at,omitempty"`
LastRequestID string `json:"last_request_id,omitempty"`
Timings *Timings `json:"timings,omitempty"`
}
type Timings struct {
ConversationCreatedAt time.Time `json:"conversation_created_at"`
RoomIssuedAt *time.Time `json:"room_issued_at,omitempty"`
DeviceActivatedAt *time.Time `json:"device_activated_at,omitempty"`
AgentActiveAt *time.Time `json:"agent_active_at,omitempty"`
StopRequestedAt *time.Time `json:"stop_requested_at,omitempty"`
ConversationEndedAt *time.Time `json:"conversation_ended_at,omitempty"`
RoomIssueMS *int64 `json:"room_issue_ms,omitempty"`
DeviceJoinMS *int64 `json:"device_join_ms,omitempty"`
AgentStartMS *int64 `json:"agent_start_ms,omitempty"`
AgentStopMS *int64 `json:"agent_stop_ms,omitempty"`
TotalMS *int64 `json:"total_ms,omitempty"`
}
type ActionResult struct {
Action string `json:"action"`
DeviceID string `json:"device_id"`
ConversationID string `json:"conversation_id,omitempty"`
ConversationStatus string `json:"conversation_status,omitempty"`
CommandID string `json:"command_id,omitempty"`
CommandStatus string `json:"command_status,omitempty"`
JobID string `json:"job_id,omitempty"`
JobType string `json:"job_type,omitempty"`
ManualRetryCount int `json:"manual_retry_count,omitempty"`
Queued bool `json:"queued"`
}
type Job struct {
ID string `json:"id"`
EventType string `json:"event_type"`
AggregateType string `json:"aggregate_type"`
Status string `json:"status"`
AttemptCount int `json:"attempt_count"`
ManualRetryCount int `json:"manual_retry_count"`
LastErrorCode string `json:"last_error_code,omitempty"`
LastManualRetryAt *time.Time `json:"last_manual_retry_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type Bundle struct {
ID string `json:"id"`
GeneratedAt time.Time `json:"generated_at"`
SearchField string `json:"search_field"`
SearchValue string `json:"search_value"`
RedactionPolicy string `json:"redaction_policy"`
Items []Record `json:"items"`
}
type ExternalDependency struct {
ServiceName string `json:"service_name"`
Dependency string `json:"dependency"`
Status string `json:"status"`
CheckedAt time.Time `json:"checked_at"`
LastSuccessAt *time.Time `json:"last_success_at,omitempty"`
LastFailureAt *time.Time `json:"last_failure_at,omitempty"`
LastErrorCode string `json:"last_error_code,omitempty"`
LatencyMS int64 `json:"latency_ms"`
ProbeIntervalMS int64 `json:"probe_interval_ms"`
ConsecutiveFailures int `json:"consecutive_failures"`
}
type Service struct {
pool *pgxpool.Pool
idem *idempotency.Executor
audit *audit.Repository
outbox *outbox.Store
}
func NewService(pool *pgxpool.Pool, idem *idempotency.Executor, auditRepository *audit.Repository) (*Service, error) {
if pool == nil || idem == nil || auditRepository == nil {
return nil, errors.New("diagnostic service dependencies are required")
}
return &Service{pool: pool, idem: idem, audit: auditRepository, outbox: outbox.NewStore(pool)}, nil
}
type actor struct{ tenantID, userID, userPublicID string }
func (s *Service) resolveActor(ctx context.Context, principal identity.Principal, tenantPublicID string, roles ...string) (actor, error) {
if _, err := uuid.Parse(tenantPublicID); err != nil {
return actor{}, ErrInvalid
}
var result actor
var role string
err := s.pool.QueryRow(ctx, `SELECT t.id::text,ua.id::text,ua.public_id::text,ua.platform_role FROM tenant t JOIN user_account ua ON ua.tenant_id=t.id AND ua.status='active' AND ua.account_kind='operator' WHERE t.public_id=$1::uuid AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3`, tenantPublicID, principal.Issuer, principal.Subject).Scan(&result.tenantID, &result.userID, &result.userPublicID, &role)
if errors.Is(err, pgx.ErrNoRows) {
return actor{}, ErrAccessDenied
}
if err != nil {
return actor{}, err
}
for _, allowed := range roles {
if role == allowed {
return result, nil
}
}
return actor{}, ErrAccessDenied
}
func (s *Service) Search(ctx context.Context, principal identity.Principal, tenantPublicID, field, value string) ([]Record, error) {
field, value = strings.TrimSpace(field), strings.TrimSpace(value)
if !searchFieldAllowed[field] || value == "" || len(value) > 200 {
return nil, ErrInvalid
}
if field == "device_id" || field == "conversation_id" {
if _, err := uuid.Parse(value); err != nil {
return nil, ErrInvalid
}
} else if !requestIDPattern.MatchString(value) {
return nil, ErrInvalid
}
currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return nil, err
}
condition := map[string]string{
"device_id": "d.public_id=$2::uuid",
"conversation_id": "c.public_id=$2::uuid",
"room_id": "rt.room_id=$2",
"provider_task_ref": "COALESCE(rt.provider_task_ref,hs.provider_session_ref)=$2",
"request_id": "EXISTS(SELECT 1 FROM audit_event ae WHERE ae.tenant_id=c.tenant_id AND ae.request_id=$2 AND ae.resource_id IN(c.id,c.public_id,d.id,d.public_id))",
}[field]
query := diagnosticSelect + " WHERE d.tenant_id=$1::uuid AND " + condition + " ORDER BY c.created_at DESC NULLS LAST LIMIT 50"
rows, err := s.pool.Query(ctx, query, currentActor.tenantID, value)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Record{}
for rows.Next() {
item, scanErr := scanRecord(rows)
if scanErr != nil {
return nil, scanErr
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) ListExternalDependencies(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]ExternalDependency, error) {
if _, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin"); err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, `SELECT service_name,dependency,status,checked_at,last_success_at,last_failure_at,COALESCE(last_error_code,''),latency_ms,probe_interval_ms,consecutive_failures FROM external_dependency_probe_state ORDER BY service_name,dependency`)
if err != nil {
return nil, err
}
defer rows.Close()
items := []ExternalDependency{}
for rows.Next() {
var item ExternalDependency
if err = rows.Scan(&item.ServiceName, &item.Dependency, &item.Status, &item.CheckedAt, &item.LastSuccessAt, &item.LastFailureAt, &item.LastErrorCode, &item.LatencyMS, &item.ProbeIntervalMS, &item.ConsecutiveFailures); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) StopConversation(ctx context.Context, principal identity.Principal, tenantPublicID, conversationPublicID, key, requestID, reason string) (ActionResult, bool, error) {
reason = strings.TrimSpace(reason)
if _, err := uuid.Parse(conversationPublicID); err != nil || reason == "" || len([]rune(reason)) > 500 {
return ActionResult{}, false, ErrInvalid
}
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_admin")
if err != nil {
return ActionResult{}, false, err
}
body, _ := json.Marshal(map[string]string{"conversation_id": conversationPublicID, "reason": reason})
result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &a.tenantID, ActorKey: "ops:" + a.userPublicID, Scope: "POST /api/v1/ops/diagnostics/conversations/stop", Key: key, RequestHash: idempotency.Hash(body), ExpiresAfter: 24 * time.Hour}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
var internalID, deviceID, status, runtimeStatus, runtimeType string
err := tx.QueryRow(operationContext, `SELECT c.id::text,d.public_id::text,c.status,c.runtime_type,COALESCE(rt.status,hs.status,'') FROM conversation c JOIN device d ON d.id=c.device_id LEFT JOIN rtc_task rt ON rt.conversation_id=c.id LEFT JOIN hardware_agent_session hs ON hs.conversation_id=c.id WHERE c.tenant_id=$1::uuid AND c.public_id=$2::uuid FOR UPDATE OF c`, a.tenantID, conversationPublicID).Scan(&internalID, &deviceID, &status, &runtimeType, &runtimeStatus)
if errors.Is(err, pgx.ErrNoRows) {
return idempotency.Result{}, ErrNotFound
}
if err != nil {
return idempotency.Result{}, err
}
queued := false
fromStatus := status
switch status {
case "COMPLETED", "FAILED", "EXPIRED", "STOPPING":
case "PREPARING", "ROOM_ISSUED":
status = "EXPIRED"
if _, err = tx.Exec(operationContext, `UPDATE conversation SET status='EXPIRED',stop_reason='ops_stop_before_start',ended_at=now(),lease_expires_at=NULL,version=version+1,updated_at=now() WHERE id=$1::uuid`, internalID); err == nil && runtimeType == "volc_hardware_agent" {
_, err = tx.Exec(operationContext, `UPDATE hardware_agent_session SET status='EXPIRED',updated_at=now() WHERE conversation_id=$1::uuid`, internalID)
} else if err == nil {
_, err = tx.Exec(operationContext, `UPDATE rtc_task SET status='COMPLETED',lease_expires_at=NULL,updated_at=now() WHERE conversation_id=$1::uuid`, internalID)
}
case "STARTING_AGENT", "ACTIVE":
status, queued = "STOPPING", runtimeType != "volc_hardware_agent"
if _, err = tx.Exec(operationContext, `UPDATE conversation SET status='STOPPING',stop_reason='ops_stop',lease_expires_at=NULL,version=version+1,updated_at=now() WHERE id=$1::uuid`, internalID); err == nil && runtimeType == "volc_hardware_agent" {
_, err = tx.Exec(operationContext, `UPDATE hardware_agent_session SET status='STOPPING',updated_at=now() WHERE conversation_id=$1::uuid`, internalID)
} else if err == nil {
_, err = tx.Exec(operationContext, `UPDATE rtc_task SET status=CASE WHEN status='ACTIVE' THEN 'STOPPING' ELSE 'UNKNOWN' END,lease_expires_at=NULL,updated_at=now() WHERE conversation_id=$1::uuid`, internalID)
}
if err == nil && runtimeType != "volc_hardware_agent" {
dedupe := "ops-stop:" + internalID
payload, _ := json.Marshal(map[string]string{"conversation_id": conversationPublicID, "reason": "ops_stop"})
_, err = s.outbox.Enqueue(operationContext, tx, outbox.NewEvent{TenantID: &a.tenantID, AggregateType: "conversation", AggregateID: internalID, EventType: "conversation.agent_stop_requested", DeduplicationKey: &dedupe, Payload: payload})
}
default:
return idempotency.Result{}, ErrStateConflict
}
if err != nil {
return idempotency.Result{}, err
}
if status != fromStatus {
if _, err = tx.Exec(operationContext, `INSERT INTO conversation_state_history(conversation_id,from_status,to_status,reason,source) VALUES($1::uuid,$2,$3,$4,'operator')`, internalID, fromStatus, status, "ops diagnostics: "+reason); err != nil {
return idempotency.Result{}, err
}
}
if err = s.audit.Record(operationContext, tx, audit.Event{TenantID: &a.tenantID, ActorType: "user", ActorPublicID: &a.userPublicID, Action: "ops.diagnostics.conversation.stop", ResourceType: "conversation", ResourceID: &conversationPublicID, RequestID: requestID, Purpose: reason, Result: "success", Detail: map[string]any{"runtime_type": runtimeType, "from_runtime_status": runtimeStatus, "to_status": status}}); err != nil {
return idempotency.Result{}, err
}
output := ActionResult{Action: "stop_conversation", DeviceID: deviceID, ConversationID: conversationPublicID, ConversationStatus: status, Queued: queued}
encoded, _ := json.Marshal(output)
return idempotency.Result{StatusCode: 200, Body: encoded, ResourceID: &internalID}, nil
})
if err != nil {
return ActionResult{}, false, err
}
var output ActionResult
if err = json.Unmarshal(result.Body, &output); err != nil {
return ActionResult{}, false, err
}
return output, !result.Replayed, nil
}
func (s *Service) ListDeadJobs(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]Job, error) {
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin")
if err != nil {
return nil, err
}
rows, err := s.pool.Query(ctx, `SELECT public_id::text,event_type,aggregate_type,status,attempt_count,manual_retry_count,COALESCE(last_error_code,''),last_manual_retry_at,created_at FROM outbox_event WHERE tenant_id=$1::uuid AND status='dead' ORDER BY created_at DESC LIMIT 100`, a.tenantID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Job{}
for rows.Next() {
var item Job
if err := rows.Scan(&item.ID, &item.EventType, &item.AggregateType, &item.Status, &item.AttemptCount, &item.ManualRetryCount, &item.LastErrorCode, &item.LastManualRetryAt, &item.CreatedAt); err != nil {
return nil, err
}
items = append(items, item)
}
return items, rows.Err()
}
func (s *Service) RetryDeadJob(ctx context.Context, principal identity.Principal, tenantPublicID, jobPublicID, key, requestID, reason string) (ActionResult, bool, error) {
reason = strings.TrimSpace(reason)
if _, err := uuid.Parse(jobPublicID); err != nil || reason == "" || len([]rune(reason)) > 500 {
return ActionResult{}, false, ErrInvalid
}
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_admin")
if err != nil {
return ActionResult{}, false, err
}
body, _ := json.Marshal(map[string]string{"job_id": jobPublicID, "reason": reason})
result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &a.tenantID, ActorKey: "ops:" + a.userPublicID, Scope: "POST /api/v1/ops/diagnostics/jobs/retry", Key: key, RequestHash: idempotency.Hash(body), ExpiresAfter: 24 * time.Hour}, func(op context.Context, tx pgx.Tx) (idempotency.Result, error) {
var internalID, eventType, status string
var manualRetries int
if queryErr := tx.QueryRow(op, `SELECT id::text,event_type,status,manual_retry_count FROM outbox_event WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE`, a.tenantID, jobPublicID).Scan(&internalID, &eventType, &status, &manualRetries); errors.Is(queryErr, pgx.ErrNoRows) {
return idempotency.Result{}, ErrNotFound
} else if queryErr != nil {
return idempotency.Result{}, queryErr
}
if status != "dead" {
return idempotency.Result{}, ErrStateConflict
}
if !retryableJobTypeAllowed[eventType] {
return idempotency.Result{}, ErrStateConflict
}
manualRetries++
if _, updateErr := tx.Exec(op, `UPDATE outbox_event SET status='pending',attempt_count=0,available_at=now(),locked_at=NULL,locked_by=NULL,dead_at=NULL,last_error_code=NULL,manual_retry_count=$2,last_manual_retry_at=now(),last_manual_retry_by=$3::uuid,updated_at=now() WHERE id=$1::uuid`, internalID, manualRetries, a.userID); updateErr != nil {
return idempotency.Result{}, updateErr
}
if auditErr := s.audit.Record(op, tx, audit.Event{TenantID: &a.tenantID, ActorType: "user", ActorPublicID: &a.userPublicID, Action: "ops.diagnostics.job.retry", ResourceType: "outbox_job", ResourceID: &jobPublicID, RequestID: requestID, Purpose: reason, Result: "success", Detail: map[string]any{"event_type": eventType, "from_status": status, "to_status": "pending", "manual_retry_count": manualRetries}}); auditErr != nil {
return idempotency.Result{}, auditErr
}
output := ActionResult{Action: "retry_job", JobID: jobPublicID, JobType: eventType, ManualRetryCount: manualRetries, Queued: true}
encoded, _ := json.Marshal(output)
return idempotency.Result{StatusCode: 200, Body: encoded, ResourceID: &internalID}, nil
})
if err != nil {
return ActionResult{}, false, err
}
var output ActionResult
if err = json.Unmarshal(result.Body, &output); err != nil {
return ActionResult{}, false, err
}
return output, !result.Replayed, nil
}
func (s *Service) RevokeDevice(ctx context.Context, principal identity.Principal, tenantPublicID, devicePublicID, key, requestID, reason string) (ActionResult, bool, error) {
reason = strings.TrimSpace(reason)
if _, err := uuid.Parse(devicePublicID); err != nil || reason == "" || len([]rune(reason)) > 500 {
return ActionResult{}, false, ErrInvalid
}
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_admin")
if err != nil {
return ActionResult{}, false, err
}
body, _ := json.Marshal(map[string]string{"device_id": devicePublicID, "reason": reason})
result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &a.tenantID, ActorKey: "ops:" + a.userPublicID, Scope: "POST /api/v1/ops/diagnostics/devices/revoke", Key: key, RequestHash: idempotency.Hash(body), ExpiresAfter: 24 * time.Hour}, func(op context.Context, tx pgx.Tx) (idempotency.Result, error) {
var deviceID, oldStatus string
if queryErr := tx.QueryRow(op, `SELECT id::text,status FROM device WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE`, a.tenantID, devicePublicID).Scan(&deviceID, &oldStatus); errors.Is(queryErr, pgx.ErrNoRows) {
return idempotency.Result{}, ErrNotFound
} else if queryErr != nil {
return idempotency.Result{}, queryErr
}
if oldStatus != "revoked" {
if _, updateErr := tx.Exec(op, `UPDATE device SET status='revoked',revoked_at=COALESCE(revoked_at,now()),version=version+1,updated_at=now() WHERE id=$1::uuid`, deviceID); updateErr != nil {
return idempotency.Result{}, updateErr
}
}
var conversationInternalID, conversationPublicID, conversationStatus, runtimeStatus, runtimeType string
queued := false
conversationQueryErr := tx.QueryRow(op, `SELECT c.id::text,c.public_id::text,c.status,c.runtime_type,COALESCE(rt.status,hs.status,'') FROM conversation c LEFT JOIN rtc_task rt ON rt.conversation_id=c.id LEFT JOIN hardware_agent_session hs ON hs.conversation_id=c.id WHERE c.tenant_id=$1::uuid AND c.device_id=$2::uuid AND c.status NOT IN('COMPLETED','FAILED','EXPIRED') ORDER BY c.created_at DESC LIMIT 1 FOR UPDATE OF c`, a.tenantID, deviceID).Scan(&conversationInternalID, &conversationPublicID, &conversationStatus, &runtimeType, &runtimeStatus)
if conversationQueryErr != nil && !errors.Is(conversationQueryErr, pgx.ErrNoRows) {
return idempotency.Result{}, conversationQueryErr
}
if conversationQueryErr == nil {
fromStatus := conversationStatus
switch conversationStatus {
case "PREPARING", "ROOM_ISSUED":
conversationStatus = "EXPIRED"
if _, updateErr := tx.Exec(op, `UPDATE conversation SET status='EXPIRED',stop_reason='device_revoked_before_start',ended_at=now(),lease_expires_at=NULL,version=version+1,updated_at=now() WHERE id=$1::uuid`, conversationInternalID); updateErr != nil {
return idempotency.Result{}, updateErr
}
query := `UPDATE rtc_task SET status='COMPLETED',lease_expires_at=NULL,updated_at=now() WHERE conversation_id=$1::uuid`
if runtimeType == "volc_hardware_agent" {
query = `UPDATE hardware_agent_session SET status='EXPIRED',updated_at=now() WHERE conversation_id=$1::uuid`
}
if _, updateErr := tx.Exec(op, query, conversationInternalID); updateErr != nil {
return idempotency.Result{}, updateErr
}
case "STARTING_AGENT", "ACTIVE":
conversationStatus, queued = "STOPPING", runtimeType != "volc_hardware_agent"
if _, updateErr := tx.Exec(op, `UPDATE conversation SET status='STOPPING',stop_reason='device_revoked',lease_expires_at=NULL,version=version+1,updated_at=now() WHERE id=$1::uuid`, conversationInternalID); updateErr != nil {
return idempotency.Result{}, updateErr
}
query := `UPDATE rtc_task SET status=CASE WHEN status='ACTIVE' THEN 'STOPPING' ELSE 'UNKNOWN' END,lease_expires_at=NULL,updated_at=now() WHERE conversation_id=$1::uuid`
if runtimeType == "volc_hardware_agent" {
query = `UPDATE hardware_agent_session SET status='STOPPING',updated_at=now() WHERE conversation_id=$1::uuid`
}
if _, updateErr := tx.Exec(op, query, conversationInternalID); updateErr != nil {
return idempotency.Result{}, updateErr
}
if runtimeType != "volc_hardware_agent" {
dedupe := "ops-stop:" + conversationInternalID
payload, _ := json.Marshal(map[string]string{"conversation_id": conversationPublicID, "reason": "device_revoked"})
if _, enqueueErr := s.outbox.Enqueue(op, tx, outbox.NewEvent{TenantID: &a.tenantID, AggregateType: "conversation", AggregateID: conversationInternalID, EventType: "conversation.agent_stop_requested", DeduplicationKey: &dedupe, Payload: payload}); enqueueErr != nil {
return idempotency.Result{}, enqueueErr
}
}
case "STOPPING":
default:
return idempotency.Result{}, ErrStateConflict
}
if conversationStatus != fromStatus {
if _, historyErr := tx.Exec(op, `INSERT INTO conversation_state_history(conversation_id,from_status,to_status,reason,source) VALUES($1::uuid,$2,$3,$4,'operator')`, conversationInternalID, fromStatus, conversationStatus, "device revoked: "+reason); historyErr != nil {
return idempotency.Result{}, historyErr
}
}
}
detail := map[string]any{"from_status": oldStatus, "to_status": "revoked", "conversation_queued": queued}
if conversationPublicID != "" {
detail["conversation_id"] = conversationPublicID
detail["conversation_status"] = conversationStatus
detail["runtime_type"] = runtimeType
detail["from_runtime_status"] = runtimeStatus
}
if auditErr := s.audit.Record(op, tx, audit.Event{TenantID: &a.tenantID, ActorType: "user", ActorPublicID: &a.userPublicID, Action: "ops.diagnostics.device.revoke", ResourceType: "device", ResourceID: &devicePublicID, RequestID: requestID, Purpose: reason, Result: "success", Detail: detail}); auditErr != nil {
return idempotency.Result{}, auditErr
}
output := ActionResult{Action: "revoke_device", DeviceID: devicePublicID, ConversationID: conversationPublicID, ConversationStatus: conversationStatus, Queued: queued}
encoded, _ := json.Marshal(output)
return idempotency.Result{StatusCode: 200, Body: encoded, ResourceID: &deviceID}, nil
})
if err != nil {
return ActionResult{}, false, err
}
var output ActionResult
if err = json.Unmarshal(result.Body, &output); err != nil {
return ActionResult{}, false, err
}
return output, !result.Replayed, nil
}
func (s *Service) ExportBundle(ctx context.Context, principal identity.Principal, tenantPublicID, field, value, key, requestID, reason string) (Bundle, bool, error) {
reason = strings.TrimSpace(reason)
if reason == "" || len([]rune(reason)) > 500 {
return Bundle{}, false, ErrInvalid
}
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_admin")
if err != nil {
return Bundle{}, false, err
}
items, err := s.Search(ctx, principal, tenantPublicID, field, value)
if err != nil {
return Bundle{}, false, err
}
body, _ := json.Marshal(map[string]string{"field": field, "value": value, "reason": reason})
result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &a.tenantID, ActorKey: "ops:" + a.userPublicID, Scope: "POST /api/v1/ops/diagnostics/bundles", Key: key, RequestHash: idempotency.Hash(body), ExpiresAfter: 24 * time.Hour}, func(op context.Context, tx pgx.Tx) (idempotency.Result, error) {
bundle := Bundle{ID: uuid.NewString(), GeneratedAt: time.Now().UTC(), SearchField: field, SearchValue: value, RedactionPolicy: "ops-diagnostic-minimal-v1", Items: items}
if auditErr := s.audit.Record(op, tx, audit.Event{TenantID: &a.tenantID, ActorType: "user", ActorPublicID: &a.userPublicID, Action: "ops.diagnostics.bundle.export", ResourceType: "diagnostic_bundle", ResourceID: &bundle.ID, RequestID: requestID, Purpose: reason, Result: "success", Detail: map[string]any{"search_field": field, "item_count": len(items), "redaction_policy": bundle.RedactionPolicy}}); auditErr != nil {
return idempotency.Result{}, auditErr
}
encoded, _ := json.Marshal(bundle)
return idempotency.Result{StatusCode: 201, Body: encoded, ResourceID: &bundle.ID}, nil
})
if err != nil {
return Bundle{}, false, err
}
var bundle Bundle
if err = json.Unmarshal(result.Body, &bundle); err != nil {
return Bundle{}, false, err
}
return bundle, !result.Replayed, nil
}
func (s *Service) ReconcileConversation(ctx context.Context, principal identity.Principal, tenantPublicID, conversationPublicID, key, requestID, reason string) (ActionResult, bool, error) {
reason = strings.TrimSpace(reason)
if _, err := uuid.Parse(conversationPublicID); err != nil || reason == "" || len([]rune(reason)) > 500 {
return ActionResult{}, false, ErrInvalid
}
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_admin")
if err != nil {
return ActionResult{}, false, err
}
body, _ := json.Marshal(map[string]string{"conversation_id": conversationPublicID, "reason": reason})
result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &a.tenantID, ActorKey: "ops:" + a.userPublicID, Scope: "POST /api/v1/ops/diagnostics/conversations/reconcile", Key: key, RequestHash: idempotency.Hash(body), ExpiresAfter: 24 * time.Hour}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
var internalID, deviceID, status, taskRef, runtimeType string
if queryErr := tx.QueryRow(operationContext, `SELECT c.id::text,d.public_id::text,c.status,COALESCE(rt.provider_task_ref,hs.provider_session_ref),c.runtime_type FROM conversation c JOIN device d ON d.id=c.device_id LEFT JOIN rtc_task rt ON rt.conversation_id=c.id LEFT JOIN hardware_agent_session hs ON hs.conversation_id=c.id WHERE c.tenant_id=$1::uuid AND c.public_id=$2::uuid FOR UPDATE OF c`, a.tenantID, conversationPublicID).Scan(&internalID, &deviceID, &status, &taskRef, &runtimeType); errors.Is(queryErr, pgx.ErrNoRows) {
return idempotency.Result{}, ErrNotFound
} else if queryErr != nil {
return idempotency.Result{}, queryErr
}
eventType := ""
if status == "STARTING_AGENT" {
eventType = "conversation.agent_start_reconcile_requested"
} else if status == "STOPPING" {
eventType = "conversation.agent_stop_reconcile_requested"
}
queued := eventType != "" && runtimeType != "volc_hardware_agent"
if queued {
dedupe := "ops-reconcile:" + taskRef + ":" + key
payload, _ := json.Marshal(map[string]string{"conversation_id": conversationPublicID, "provider_task_ref": taskRef, "reason": "ops_reconcile"})
if _, enqueueErr := s.outbox.Enqueue(operationContext, tx, outbox.NewEvent{TenantID: &a.tenantID, AggregateType: "conversation", AggregateID: internalID, EventType: eventType, DeduplicationKey: &dedupe, Payload: payload}); enqueueErr != nil {
return idempotency.Result{}, enqueueErr
}
}
if auditErr := s.audit.Record(operationContext, tx, audit.Event{TenantID: &a.tenantID, ActorType: "user", ActorPublicID: &a.userPublicID, Action: "ops.diagnostics.conversation.reconcile", ResourceType: "conversation", ResourceID: &conversationPublicID, RequestID: requestID, Purpose: reason, Result: "success", Detail: map[string]any{"status": status, "runtime_type": runtimeType, "queued": queued}}); auditErr != nil {
return idempotency.Result{}, auditErr
}
output := ActionResult{Action: "reconcile_conversation", DeviceID: deviceID, ConversationID: conversationPublicID, ConversationStatus: status, Queued: queued}
encoded, _ := json.Marshal(output)
return idempotency.Result{StatusCode: 200, Body: encoded, ResourceID: &internalID}, nil
})
if err != nil {
return ActionResult{}, false, err
}
var output ActionResult
if err = json.Unmarshal(result.Body, &output); err != nil {
return ActionResult{}, false, err
}
return output, !result.Replayed, nil
}
func (s *Service) RefreshDeviceConfig(ctx context.Context, principal identity.Principal, tenantPublicID, devicePublicID, key, requestID, reason string) (ActionResult, bool, error) {
reason = strings.TrimSpace(reason)
if _, err := uuid.Parse(devicePublicID); err != nil || reason == "" || len([]rune(reason)) > 500 {
return ActionResult{}, false, ErrInvalid
}
a, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_operator", "ops_admin")
if err != nil {
return ActionResult{}, false, err
}
body, _ := json.Marshal(map[string]string{"device_id": devicePublicID, "reason": reason})
result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: &a.tenantID, ActorKey: "ops:" + a.userPublicID, Scope: "POST /api/v1/ops/diagnostics/devices/config-refresh", Key: key, RequestHash: idempotency.Hash(body), ExpiresAfter: 24 * time.Hour}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
var deviceID string
var revision int64
if queryErr := tx.QueryRow(operationContext, `SELECT d.id::text,dt.desired_revision FROM device d JOIN device_twin dt ON dt.device_id=d.id AND dt.tenant_id=d.tenant_id WHERE d.tenant_id=$1::uuid AND d.public_id=$2::uuid AND d.status='registered' FOR SHARE OF d`, a.tenantID, devicePublicID).Scan(&deviceID, &revision); errors.Is(queryErr, pgx.ErrNoRows) {
return idempotency.Result{}, ErrNotFound
} else if queryErr != nil {
return idempotency.Result{}, queryErr
}
commandPublicID := uuid.NewString()
commandInternalID := uuid.NewString()
payload, _ := json.Marshal(map[string]any{"desired_revision": revision, "reason_code": "ops_config_refresh"})
digest := idempotency.Hash(payload)
_, insertErr := tx.Exec(operationContext, `INSERT INTO device_command(id,public_id,tenant_id,device_id,command_type,payload,status,requested_by,idempotency_key,request_digest,expires_at) VALUES($1::uuid,$2::uuid,$3::uuid,$4::uuid,'apply_config',$5::jsonb,'QUEUED',$6::uuid,$7,$8,now()+interval '1 hour')`, commandInternalID, commandPublicID, a.tenantID, deviceID, payload, a.userID, "ops-refresh:"+key, digest)
if insertErr != nil {
return idempotency.Result{}, insertErr
}
if auditErr := s.audit.Record(operationContext, tx, audit.Event{TenantID: &a.tenantID, ActorType: "user", ActorPublicID: &a.userPublicID, Action: "ops.diagnostics.device.config_refresh", ResourceType: "device", ResourceID: &devicePublicID, RequestID: requestID, Purpose: reason, Result: "success", Detail: map[string]any{"desired_revision": revision, "command_id": commandPublicID}}); auditErr != nil {
return idempotency.Result{}, auditErr
}
output := ActionResult{Action: "refresh_config", DeviceID: devicePublicID, CommandID: commandPublicID, CommandStatus: "QUEUED", Queued: true}
encoded, _ := json.Marshal(output)
return idempotency.Result{StatusCode: 201, Body: encoded, ResourceID: &commandInternalID}, nil
})
if err != nil {
return ActionResult{}, false, err
}
var output ActionResult
if err = json.Unmarshal(result.Body, &output); err != nil {
return ActionResult{}, false, err
}
return output, !result.Replayed, nil
}
const diagnosticSelect = `SELECT
d.public_id::text,d.status,COALESCE(dp.online,false),COALESCE(dt.firmware_version,''),
COALESCE(dt.desired_revision,1),dt.reported_revision,COALESCE(dt.last_error_code,''),dt.last_error_at,
COALESCE(c.public_id::text,''),COALESCE(c.status,''),COALESCE(c.failure_code,''),c.allocation_expires_at,
rt.credential_issued_at,rt.credential_expires_at,
COALESCE(rt.room_id,''),COALESCE(rt.provider,hs.provider,''),COALESCE(rt.provider_task_ref,hs.provider_session_ref,''),COALESCE(rt.status,hs.status,''),
COALESCE(rt.start_attempts,0),COALESCE(rt.stop_attempts,0),COALESCE(pe.event_count,0),
COALESCE(pe.event_type,''),COALESCE(pe.event_status,''),pe.received_at,COALESCE(ae.request_id,''),
c.created_at,ch.room_issued_at,ch.device_activated_at,ch.agent_active_at,ch.stop_requested_at,c.ended_at
FROM device d
LEFT JOIN device_twin dt ON dt.device_id=d.id AND dt.tenant_id=d.tenant_id
LEFT JOIN device_presence_state dp ON dp.device_id=d.id AND dp.tenant_id=d.tenant_id
LEFT JOIN conversation c ON c.device_id=d.id AND c.tenant_id=d.tenant_id
LEFT JOIN rtc_task rt ON rt.conversation_id=c.id
LEFT JOIN hardware_agent_session hs ON hs.conversation_id=c.id
LEFT JOIN LATERAL (
SELECT count(*) OVER() event_count,event_type,event_status,received_at FROM (
SELECT event_type,COALESCE(sanitized_payload->>'status','') event_status,received_at
FROM provider_event WHERE conversation_id=c.id
UNION ALL
SELECT event_type,event_type,occurred_at FROM hardware_agent_event WHERE conversation_id=c.id
) runtime_event ORDER BY received_at DESC LIMIT 1
) pe ON true
LEFT JOIN LATERAL (
SELECT
MIN(occurred_at) FILTER (WHERE to_status='ROOM_ISSUED') room_issued_at,
MIN(occurred_at) FILTER (WHERE to_status='STARTING_AGENT') device_activated_at,
MIN(occurred_at) FILTER (WHERE to_status='ACTIVE') agent_active_at,
MIN(occurred_at) FILTER (WHERE to_status='STOPPING') stop_requested_at
FROM conversation_state_history WHERE conversation_id=c.id
) ch ON c.id IS NOT NULL
LEFT JOIN LATERAL (
SELECT request_id FROM audit_event
WHERE tenant_id=d.tenant_id AND resource_id IN(d.id,d.public_id,c.id,c.public_id) AND request_id IS NOT NULL
ORDER BY occurred_at DESC LIMIT 1
) ae ON true`
type rowScanner interface{ Scan(...any) error }
func scanRecord(row rowScanner) (Record, error) {
var item Record
var createdAt, roomIssuedAt, deviceActivatedAt, agentActiveAt, stopRequestedAt, endedAt *time.Time
err := row.Scan(&item.DeviceID, &item.DeviceStatus, &item.DeviceOnline, &item.FirmwareVersion, &item.DesiredConfigRevision, &item.ReportedConfigRevision, &item.LastDeviceErrorCode, &item.LastDeviceErrorAt, &item.ConversationID, &item.ConversationStatus, &item.ConversationFailureCode, &item.AllocationExpiresAt, &item.CredentialIssuedAt, &item.CredentialExpiresAt, &item.RoomID, &item.Provider, &item.ProviderTaskRef, &item.ProviderTaskStatus, &item.ProviderStartAttempts, &item.ProviderStopAttempts, &item.ProviderNotificationCount, &item.LastProviderEvent, &item.LastProviderEventStatus, &item.LastProviderEventAt, &item.LastRequestID, &createdAt, &roomIssuedAt, &deviceActivatedAt, &agentActiveAt, &stopRequestedAt, &endedAt)
if err == nil && createdAt != nil {
item.Timings = &Timings{
ConversationCreatedAt: *createdAt,
RoomIssuedAt: roomIssuedAt,
DeviceActivatedAt: deviceActivatedAt,
AgentActiveAt: agentActiveAt,
StopRequestedAt: stopRequestedAt,
ConversationEndedAt: endedAt,
RoomIssueMS: elapsedMilliseconds(createdAt, roomIssuedAt),
DeviceJoinMS: elapsedMilliseconds(roomIssuedAt, deviceActivatedAt),
AgentStartMS: elapsedMilliseconds(deviceActivatedAt, agentActiveAt),
AgentStopMS: elapsedMilliseconds(stopRequestedAt, endedAt),
TotalMS: elapsedMilliseconds(createdAt, endedAt),
}
}
return item, err
}
func elapsedMilliseconds(start, end *time.Time) *int64 {
if start == nil || end == nil || end.Before(*start) {
return nil
}
value := end.Sub(*start).Milliseconds()
return &value
}
func (r Record) String() string {
return fmt.Sprintf("device=%s conversation=%s", r.DeviceID, r.ConversationID)
}