2026-09-06 17:04:11 +08:00
package orchestration
import (
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/conversation"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/idempotency"
"data-terminal-cloud/internal/outbox"
"github.com/jackc/pgx/v5"
)
var (
ErrHardwareSessionProof = errors . New ( "hardware agent session proof is invalid" )
ErrHardwareEventConflict = errors . New ( "hardware agent event conflicts with stored event" )
2026-10-08 10:06:44 +08:00
ErrCallContractRequired = errors . New ( "CALL-MEMORY-1.1 required after a critical memory change" )
2026-09-06 17:04:11 +08:00
)
func ( s * Service ) CreateHardwareAgent ( ctx context . Context , identity deviceauth . Identity , idempotencyKey , requestID string , rawRequest [] byte , input HardwareAgentCreateInput ) ( HardwareAgentCreateResponse , bool , error ) {
tenantID := identity . TenantID
request := idempotency . Request { TenantID : & tenantID , ActorKey : "device:" + identity . PublicID ,
Scope : "POST /device/v3/agent-sessions" , Key : idempotencyKey ,
RequestHash : idempotency . Hash ( rawRequest ), ExpiresAfter : 24 * time . Hour }
memoryVector , memoryPolicy , err := s . prepareMemoryQuery ( ctx , identity , input . RequestedGoal )
if err != nil {
return HardwareAgentCreateResponse {}, false , err
}
var vikingSubject , vikingContext string
2026-09-06 17:27:11 +08:00
var vikingNativeMemory * HardwareAgentMemoryConfig
2026-09-06 17:04:11 +08:00
if s . viking != nil {
readContext , cancel := context . WithTimeout ( ctx , 3 * time . Second )
2026-09-06 17:27:11 +08:00
native , readErr := s . viking . NativeAccessForDevice ( readContext , identity . TenantID , identity . ID )
2026-09-06 17:04:11 +08:00
cancel ()
if readErr != nil {
2026-09-06 17:27:11 +08:00
slog . WarnContext ( ctx , "Viking native memory configuration unavailable" , "error" , readErr )
} else if native != nil {
vikingSubject = native . SubjectID
vikingNativeMemory = & HardwareAgentMemoryConfig {
CollectionName : native . CollectionName , UserID : native . UserID , AssistantID : native . AssistantID ,
MemoryTypes : native . MemoryTypes , Limit : native . Limit , TransitionWords : native . TransitionWords ,
}
} else {
// Older deployments without a native collection name keep the
// platform-side startup retrieval path as a compatibility fallback.
fallbackContext , fallbackCancel := context . WithTimeout ( ctx , 3 * time . Second )
vikingSubject , vikingContext , readErr = s . viking . PrepareForDevice ( fallbackContext , identity . TenantID , identity . ID )
fallbackCancel ()
if readErr != nil {
slog . WarnContext ( ctx , "Viking startup memory unavailable" , "error" , readErr )
}
2026-09-06 17:04:11 +08:00
}
}
2026-09-12 08:57:29 +08:00
result , err := s . executor . ExecuteGuarded ( ctx , request , onboardingCreateGuard ( identity , input ), func ( operationContext context . Context , tx pgx . Tx ) ( idempotency . Result , error ) {
2026-09-06 17:04:11 +08:00
now := s . now (). UTC ()
created , internalID , createErr := s . repository . Create ( operationContext , tx , identity , CreateInput {
DeviceTime : input . DeviceTime , MonotonicMS : input . MonotonicMS , Network : input . Network ,
RequestedGoal : input . RequestedGoal , ConfigRevision : input . ConfigRevision ,
Capabilities : Capabilities { UploadResume : boolPointer ( input . Capabilities . TranscriptUpload )},
}, AllocationOptions { Now : now , AllocationExpires : now . Add ( s . options . AllocationTTL ),
TokenExpires : now . Add ( s . options . TokenTTL ), RTCAppID : s . options . RTCAppID ,
VoiceProvider : s . options . VoiceProvider , EngineMode : s . options . EngineMode ,
HeartbeatInterval : s . options . HeartbeatInterval , MemoryQueryVector : memoryVector ,
2026-09-06 17:27:11 +08:00
VikingSubjectID : vikingSubject , VikingContext : vikingContext , VikingNativeMemory : vikingNativeMemory ,
2026-09-06 17:04:11 +08:00
MemoryPolicy : memoryPolicy , RequirePublishedRouting : s . options . RequirePublishedRouting ,
RuntimeType : RuntimeVolcHardwareAgent , HardwareBotID : s . options . HardwareBotID ,
HardwareTransport : s . options . HardwareTransport , HardwareCapabilities : input . Capabilities ,
SubtitleCallbackURL : s . options . SubtitleCallbackURL , SubtitleCallbackSecret : s . options . SubtitleCallbackSecret ,
SubtitlePrimary : s . options . SubtitlePrimary ,
HardwareMemoryToolURL : s . hardwareMemoryToolURL })
if createErr != nil {
return idempotency . Result {}, createErr
}
2026-09-12 08:57:29 +08:00
if err := attachOnboardingConversation ( operationContext , tx , identity , input , internalID ); err != nil {
return idempotency . Result {}, err
}
2026-09-06 17:04:11 +08:00
if created . Hardware == nil {
return idempotency . Result {}, errors . New ( "hardware agent runtime was not allocated" )
}
2026-09-12 08:57:29 +08:00
if input . Intent == "activation_test" {
created . Hardware . Session . MaximumDurationSeconds = 60
if created . Hardware . Session . AllocationExpiresAt . After ( now . Add ( time . Minute )) {
created . Hardware . Session . AllocationExpiresAt = now . Add ( time . Minute )
}
}
2026-09-06 17:04:11 +08:00
body , marshalErr := json . Marshal ( created . Hardware )
if marshalErr != nil {
return idempotency . Result {}, marshalErr
}
actorID := identity . PublicID
if auditErr := s . audit . Record ( operationContext , tx , audit . Event { TenantID : & tenantID ,
ActorType : "device" , ActorPublicID : & actorID , Action : "hardware_agent.session.create" ,
ResourceType : "conversation" , ResourceID : & internalID , RequestID : requestID ,
Purpose : input . RequestedGoal , Result : "success" , Detail : map [ string ] any {
"runtime_type" : RuntimeVolcHardwareAgent , "transport" : created . Hardware . Runtime . Transport ,
"prompt_bundle_version" : created . Hardware . SessionConfig . PromptBundleVersion ,
}}); auditErr != nil {
return idempotency . Result {}, auditErr
}
return idempotency . Result { StatusCode : http . StatusCreated , Body : body , ResourceID : & internalID }, nil
})
if err != nil {
return HardwareAgentCreateResponse {}, false , err
}
return s . finalizeHardwareCreate ( result )
}
func ( s * Service ) finalizeHardwareCreate ( result idempotency . Result ) ( HardwareAgentCreateResponse , bool , error ) {
response , replayed , err := decodeHardwareCreate ( result )
if err != nil || s . hardwareMemoryTool == nil || s . hardwareMemoryToolURL == "" {
return response , replayed , err
}
token , err := s . hardwareMemoryTool . Issue ( response . ConversationID , response . Runtime . ProviderSessionRef , s . now (). UTC (). Add ( s . hardwareMemoryToolTTL ))
if err != nil {
return HardwareAgentCreateResponse {}, replayed , fmt . Errorf ( "issue hardware memory tool token: %w" , err )
}
response . SessionConfig . ToolURL = s . hardwareMemoryToolURL
response . SessionConfig . ToolScopeToken = token
return response , replayed , nil
}
func decodeHardwareCreate ( result idempotency . Result ) ( HardwareAgentCreateResponse , bool , error ) {
var response HardwareAgentCreateResponse
if err := json . Unmarshal ( result . Body , & response ); err != nil {
return HardwareAgentCreateResponse {}, result . Replayed , fmt . Errorf ( "decode stored hardware agent session: %w" , err )
}
return response , result . Replayed , nil
}
func boolPointer ( value bool ) * bool { return & value }
func ( s * Service ) ProcessHardwareAgentEvent ( ctx context . Context , identity deviceauth . Identity , conversationPublicID string , rawBody [] byte , input HardwareAgentEventInput ) ( HardwareAgentEventResponse , error ) {
if s . pool == nil {
return HardwareAgentEventResponse {}, errors . New ( "hardware agent event database is unavailable" )
}
tx , err := s . pool . BeginTx ( ctx , pgx . TxOptions {})
if err != nil {
return HardwareAgentEventResponse {}, err
}
defer tx . Rollback ( context . Background ())
2026-09-12 08:57:29 +08:00
if err := guardOnboardingEvent ( ctx , tx , identity , conversationPublicID , input ); err != nil {
return HardwareAgentEventResponse {}, err
}
2026-09-06 17:04:11 +08:00
now := s . now (). UTC ()
var internalID , tenantID , providerSessionRef , nonceDigest , expectedBotID , expectedTransport , expectedConfigDigest string
var lastEventSequence int64
var currentStatus conversation . Status
var allocationExpires time . Time
err = tx . QueryRow ( ctx , `
SELECT c.id::text,c.tenant_id::text,c.status,c.allocation_expires_at,
hs.provider_session_ref,hs.session_nonce_digest,hs.last_event_sequence,hs.bot_id,hs.transport,hs.config_digest
FROM conversation c JOIN hardware_agent_session hs ON hs.conversation_id=c.id AND hs.tenant_id=c.tenant_id
WHERE c.public_id=$1::uuid AND c.tenant_id=$2::uuid AND c.device_id=$3::uuid
FOR UPDATE OF c,hs` , conversationPublicID , identity . TenantID , identity . ID ). Scan (
& internalID , & tenantID , & currentStatus , & allocationExpires , & providerSessionRef , & nonceDigest , & lastEventSequence ,
& expectedBotID , & expectedTransport , & expectedConfigDigest )
if errors . Is ( err , pgx . ErrNoRows ) {
return HardwareAgentEventResponse {}, ErrConversationNotFound
}
if err != nil {
return HardwareAgentEventResponse {}, fmt . Errorf ( "read hardware agent session: %w" , err )
}
providedDigest := sha256 . Sum256 ([] byte ( input . SessionNonce ))
expectedDigest , decodeErr := hex . DecodeString ( nonceDigest )
if decodeErr != nil || len ( expectedDigest ) != len ( providedDigest ) || subtle . ConstantTimeCompare ( expectedDigest , providedDigest [:]) != 1 || input . ProviderSessionRef != providerSessionRef {
return HardwareAgentEventResponse {}, ErrHardwareSessionProof
}
payloadDigest := sha256 . Sum256 ( rawBody )
digestText := hex . EncodeToString ( payloadDigest [:])
var storedDigest string
err = tx . QueryRow ( ctx , `SELECT payload_digest FROM hardware_agent_event WHERE tenant_id=$1::uuid AND event_id=$2` , tenantID , input . EventID ). Scan ( & storedDigest )
if err == nil {
if storedDigest != digestText {
return HardwareAgentEventResponse {}, ErrHardwareEventConflict
}
if err = tx . Commit ( ctx ); err != nil {
return HardwareAgentEventResponse {}, err
}
return HardwareAgentEventResponse { ConversationID : conversationPublicID , Status : currentStatus , Action : hardwareAgentAction ( currentStatus ), Duplicate : true , AcceptedAt : now }, nil
}
if ! errors . Is ( err , pgx . ErrNoRows ) {
return HardwareAgentEventResponse {}, err
}
if input . SequenceNo <= lastEventSequence {
return HardwareAgentEventResponse {}, ErrHardwareEventConflict
}
_ , err = tx . Exec ( ctx , `INSERT INTO hardware_agent_event(
tenant_id,conversation_id,event_id,sequence_no,event_type,occurred_at,payload_digest)
VALUES($1::uuid,$2::uuid,$3,$4,$5,$6,$7)` ,
tenantID , internalID , input . EventID , input . SequenceNo , input . EventType , input . OccurredAt , digestText )
if err != nil {
return HardwareAgentEventResponse {}, fmt . Errorf ( "store hardware agent event: %w" , err )
}
if input . EventType == "session_started" {
if currentStatus != conversation . StatusRoomIssued || ! allocationExpires . After ( now ) {
return HardwareAgentEventResponse {}, ErrConversationStateConflict
}
if ! input . DynamicConfigApplied || input . BotID != expectedBotID || input . Transport != expectedTransport ||
input . ConfigDigest != expectedConfigDigest || strings . TrimSpace ( input . ProviderConnectionRef ) == "" {
return HardwareAgentEventResponse {}, ErrHardwareSessionProof
}
leaseExpires := now . Add ( time . Duration ( s . options . HeartbeatInterval * 5 ) * time . Second )
_ , err = tx . Exec ( ctx , `UPDATE conversation SET status='ACTIVE',started_at=COALESCE(started_at,$2),lease_expires_at=$3,version=version+1,updated_at=$2 WHERE id=$1::uuid` , internalID , now , leaseExpires )
if err == nil {
_ , err = tx . Exec ( ctx , `UPDATE hardware_agent_session SET status='ACTIVE',provider_started_at=COALESCE(provider_started_at,$2),last_event_at=$3,last_event_sequence=$4,updated_at=$3,provider_connection_ref=$5,dynamic_config_applied_at=$2 WHERE conversation_id=$1::uuid` , internalID , input . OccurredAt , now , input . SequenceNo , input . ProviderConnectionRef )
}
if err == nil {
_ , err = tx . Exec ( ctx , `INSERT INTO conversation_state_history(conversation_id,from_status,to_status,reason,source) VALUES($1::uuid,'ROOM_ISSUED','ACTIVE','device confirmed hardware agent session started','device')` , internalID )
}
currentStatus = conversation . StatusActive
} else if input . EventType == "session_ended" {
if currentStatus != conversation . StatusActive && currentStatus != conversation . StatusStopping {
return HardwareAgentEventResponse {}, ErrConversationStateConflict
}
fromStatus := currentStatus
_ , err = tx . Exec ( ctx , `UPDATE conversation SET status='COMPLETED',ended_at=COALESCE(ended_at,$2),lease_expires_at=NULL,stop_reason=COALESCE(stop_reason,'hardware_agent_ended'),version=version+1,updated_at=$2 WHERE id=$1::uuid` , internalID , now )
if err == nil {
_ , err = tx . Exec ( ctx , `UPDATE hardware_agent_session SET status='COMPLETED',provider_ended_at=$2,last_event_at=$3,last_event_sequence=$4,updated_at=$3 WHERE conversation_id=$1::uuid` , internalID , input . OccurredAt , now , input . SequenceNo )
}
if err == nil {
_ , err = tx . Exec ( ctx , `INSERT INTO conversation_state_history(conversation_id,from_status,to_status,reason,source) VALUES($1::uuid,$2,'COMPLETED','device confirmed hardware agent session ended','device')` , internalID , fromStatus )
}
if err == nil {
err = enqueueHardwareContentExtraction ( ctx , tx , s . outbox , tenantID , internalID )
}
currentStatus = conversation . StatusCompleted
} else if input . EventType == "session_failed" {
if conversation . IsTerminal ( currentStatus ) {
return HardwareAgentEventResponse {}, ErrConversationStateConflict
}
failureCode := input . ErrorCode
if failureCode == "" {
failureCode = "hardware_agent_failed"
}
_ , err = tx . Exec ( ctx , `UPDATE conversation SET status='FAILED',ended_at=$2,lease_expires_at=NULL,failure_code=$3,version=version+1,updated_at=$2 WHERE id=$1::uuid` , internalID , now , failureCode )
if err == nil {
_ , err = tx . Exec ( ctx , `UPDATE hardware_agent_session SET status='FAILED',provider_ended_at=$2,last_event_at=$3,last_event_sequence=$4,updated_at=$3 WHERE conversation_id=$1::uuid` , internalID , input . OccurredAt , now , input . SequenceNo )
}
if err == nil {
_ , err = tx . Exec ( ctx , `INSERT INTO conversation_state_history(conversation_id,from_status,to_status,reason,source) VALUES($1::uuid,$2,'FAILED','device reported hardware agent session failure','device')` , internalID , currentStatus )
}
if err == nil {
err = enqueueHardwareContentExtraction ( ctx , tx , s . outbox , tenantID , internalID )
}
currentStatus = conversation . StatusFailed
} else {
_ , err = tx . Exec ( ctx , `UPDATE hardware_agent_session SET last_event_at=$2,last_event_sequence=GREATEST(last_event_sequence,$3),updated_at=$2 WHERE conversation_id=$1::uuid` , internalID , now , input . SequenceNo )
2026-09-12 08:57:29 +08:00
if input . EventType == "first_audio_received" && currentStatus != conversation . StatusActive {
return HardwareAgentEventResponse {}, ErrConversationStateConflict
}
2026-09-06 17:04:11 +08:00
if err == nil && currentStatus == conversation . StatusActive {
leaseExpires := now . Add ( time . Duration ( s . options . HeartbeatInterval * 5 ) * time . Second )
_ , err = tx . Exec ( ctx , `UPDATE conversation SET lease_expires_at=$2,updated_at=$3,last_heartbeat_seq=GREATEST(last_heartbeat_seq,$4) WHERE id=$1::uuid AND status='ACTIVE'` , internalID , leaseExpires , now , input . SequenceNo )
}
}
if err != nil {
return HardwareAgentEventResponse {}, fmt . Errorf ( "apply hardware agent event: %w" , err )
}
actorID := identity . PublicID
if err = s . audit . Record ( ctx , tx , audit . Event { TenantID : & tenantID , ActorType : "device" , ActorPublicID : & actorID ,
Action : "hardware_agent.session.event" , ResourceType : "conversation" , ResourceID : & internalID ,
Result : "success" , Detail : map [ string ] any { "event_id" : input . EventID , "event_type" : input . EventType ,
"sequence_no" : input . SequenceNo , "status" : currentStatus }}); err != nil {
return HardwareAgentEventResponse {}, err
}
if err = tx . Commit ( ctx ); err != nil {
return HardwareAgentEventResponse {}, err
}
return HardwareAgentEventResponse { ConversationID : conversationPublicID , Status : currentStatus , Action : hardwareAgentAction ( currentStatus ), AcceptedAt : now }, nil
}
// Schedule the latest stored turns when a hardware session ends, including
// abrupt power loss. An extraction version below max(sequence_no) is ignored
// by the processor, so a constant version of 1 loses multi-turn summaries.
func enqueueHardwareContentExtraction ( ctx context . Context , tx pgx . Tx , store * outbox . Store , tenantID , internalID string ) error {
var version int64
if err := tx . QueryRow ( ctx , `SELECT COALESCE(max(sequence_no),0) FROM conversation_turn WHERE conversation_id=$1::uuid AND tenant_id=$2::uuid` , internalID , tenantID ). Scan ( & version ); err != nil {
return err
}
if version == 0 {
return nil
}
dedup := fmt . Sprintf ( "content-extraction-v1:%s:hardware-terminal:%d" , internalID , version )
payload , err := json . Marshal ( map [ string ] int64 { "generation_version" : version })
if err != nil {
return err
}
_ , err = store . Enqueue ( ctx , tx , outbox . NewEvent { TenantID : & tenantID , AggregateType : "conversation" , AggregateID : internalID ,
EventType : "conversation.content_extraction_requested" , DeduplicationKey : & dedup , Payload : payload })
return err
}
func hardwareAgentAction ( status conversation . Status ) string {
if status == conversation . StatusStopping || conversation . IsTerminal ( status ) {
return "stop_session"
}
return "continue"
}