@@ -191,7 +191,7 @@ func (s *Service) Search(ctx context.Context, principal identity.Principal, tena
|
||||
"device_id": "d.public_id=$2::uuid",
|
||||
"conversation_id": "c.public_id=$2::uuid",
|
||||
"room_id": "rt.room_id=$2",
|
||||
"provider_task_ref": "rt.provider_task_ref=$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"
|
||||
@@ -242,8 +242,8 @@ func (s *Service) StopConversation(ctx context.Context, principal identity.Princ
|
||||
}
|
||||
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, rtcStatus string
|
||||
err := tx.QueryRow(operationContext, `SELECT c.id::text,d.public_id::text,c.status,rt.status FROM conversation c JOIN device d ON d.id=c.device_id JOIN rtc_task rt ON rt.conversation_id=c.id WHERE c.tenant_id=$1::uuid AND c.public_id=$2::uuid FOR UPDATE OF c,rt`, a.tenantID, conversationPublicID).Scan(&internalID, &deviceID, &status, &rtcStatus)
|
||||
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
|
||||
}
|
||||
@@ -256,15 +256,19 @@ func (s *Service) StopConversation(ctx context.Context, principal identity.Princ
|
||||
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 {
|
||||
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", true
|
||||
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 {
|
||||
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 {
|
||||
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})
|
||||
@@ -280,7 +284,7 @@ func (s *Service) StopConversation(ctx context.Context, principal identity.Princ
|
||||
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{"from_rtc_status": rtcStatus, "to_status": status}}); err != nil {
|
||||
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}
|
||||
@@ -386,9 +390,9 @@ func (s *Service) RevokeDevice(ctx context.Context, principal identity.Principal
|
||||
}
|
||||
}
|
||||
|
||||
var conversationInternalID, conversationPublicID, conversationStatus, rtcStatus string
|
||||
var conversationInternalID, conversationPublicID, conversationStatus, runtimeStatus, runtimeType string
|
||||
queued := false
|
||||
conversationQueryErr := tx.QueryRow(op, `SELECT c.id::text,c.public_id::text,c.status,rt.status FROM conversation c JOIN rtc_task rt ON rt.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,rt`, a.tenantID, deviceID).Scan(&conversationInternalID, &conversationPublicID, &conversationStatus, &rtcStatus)
|
||||
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
|
||||
}
|
||||
@@ -400,21 +404,31 @@ func (s *Service) RevokeDevice(ctx context.Context, principal identity.Principal
|
||||
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
|
||||
}
|
||||
if _, updateErr := tx.Exec(op, `UPDATE rtc_task SET status='COMPLETED',lease_expires_at=NULL,updated_at=now() WHERE conversation_id=$1::uuid`, conversationInternalID); updateErr != nil {
|
||||
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", true
|
||||
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
|
||||
}
|
||||
if _, updateErr := tx.Exec(op, `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`, conversationInternalID); updateErr != nil {
|
||||
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
|
||||
}
|
||||
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
|
||||
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:
|
||||
@@ -431,7 +445,8 @@ func (s *Service) RevokeDevice(ctx context.Context, principal identity.Principal
|
||||
if conversationPublicID != "" {
|
||||
detail["conversation_id"] = conversationPublicID
|
||||
detail["conversation_status"] = conversationStatus
|
||||
detail["from_rtc_status"] = rtcStatus
|
||||
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
|
||||
@@ -493,8 +508,8 @@ func (s *Service) ReconcileConversation(ctx context.Context, principal identity.
|
||||
}
|
||||
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 string
|
||||
if queryErr := tx.QueryRow(operationContext, `SELECT c.id::text,d.public_id::text,c.status,rt.provider_task_ref FROM conversation c JOIN device d ON d.id=c.device_id JOIN rtc_task rt ON rt.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); errors.Is(queryErr, pgx.ErrNoRows) {
|
||||
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
|
||||
@@ -505,7 +520,7 @@ func (s *Service) ReconcileConversation(ctx context.Context, principal identity.
|
||||
} else if status == "STOPPING" {
|
||||
eventType = "conversation.agent_stop_reconcile_requested"
|
||||
}
|
||||
queued := eventType != ""
|
||||
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"})
|
||||
@@ -513,7 +528,7 @@ func (s *Service) ReconcileConversation(ctx context.Context, principal identity.
|
||||
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, "queued": queued}}); auditErr != nil {
|
||||
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}
|
||||
@@ -578,7 +593,7 @@ const diagnosticSelect = `SELECT
|
||||
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,''),COALESCE(rt.provider_task_ref,''),COALESCE(rt.status,''),
|
||||
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
|
||||
@@ -587,9 +602,14 @@ 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,COALESCE(sanitized_payload->>'status','') event_status,received_at
|
||||
FROM provider_event WHERE conversation_id=c.id ORDER BY received_at DESC LIMIT 1
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user