handlePayload method
Feed bytes from the LiveKit data-channel onData callback here.
Implementation
Future<void> handlePayload(Uint8List bytes) async {
Map<String, dynamic>? json;
try {
json = jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>;
} catch (_) {
return;
}
final type = json['type'] as String?;
if (type == null) return;
if (WireLog.enabled) {
WireLog.emit(
'${WireLog.prefix} ◀ RECV] type=$type bytes=${bytes.length}',
body: jsonEncode(redactForLog(json)),
);
}
const gatedTypes = {
MissionMessage.kType,
'highlight',
};
if (gatedTypes.contains(type) &&
!ActionGates.instance.actionIntelligenceEnabled) {
if (WireLog.enabled) {
debugPrint('${WireLog.prefix} ◀ DROP] type=$type '
'(actionIntelligence disabled by action_config)');
}
return;
}
switch (type) {
case MissionMessage.kType:
// Defensive parse: a malformed step still dispatches — the runner
// answers `failed failure_reason=invalid_mission` instead of the
// frame vanishing while the backend waits out its timeout. Only a
// frame with no usable mission_id (nothing to answer) is dropped.
final MissionMessage? mission;
try {
mission = MissionMessage.tryParse(json);
} catch (e, st) {
debugPrint('[VoiceLoop] mission parse failed: $e\n$st');
break;
}
if (mission == null) {
debugPrint('[VoiceLoop] mission frame without a mission_id — dropped');
break;
}
try {
await _startMission(mission);
} catch (e, st) {
debugPrint('[VoiceLoop] mission dispatch failed: $e\n$st');
}
break;
case CancelMessage.kType:
try {
_cancelMission(CancelMessage.fromJson(json));
} catch (e, st) {
debugPrint('[VoiceLoop] cancel parse failed: $e\n$st');
}
break;
case SpeakMessage.kType:
break;
case HighlightMessage.kType:
final HighlightMessage msg;
try {
msg = HighlightMessage.fromJson(json);
} catch (e, st) {
debugPrint('[VoiceLoop] highlight parse failed: $e\n$st');
break;
}
await _handleHighlight(msg);
break;
case 'request_ui_tree':
onRequestSnapshot?.call();
break;
default:
break;
}
}