handleLine method
Processes one inbound NDJSON line; every reply is emitted through
emit as a complete NDJSON line. Never throws for message-level
problems — they become error envelopes.
Implementation
Future<void> handleLine(
String line, {
required FutureOr<void> Function(String reply) emit,
}) async {
final trimmed = line.trim();
if (trimmed.isEmpty) return;
Map<String, Object?>? decoded;
try {
decoded = ZapProtocol.decodeLine(trimmed);
} on FormatException catch (e) {
_emitError(
emit,
code: 'schema',
message:
'line is not a valid ZAP '
'NDJSON message: ${e.message}',
details: [e.toString()],
);
return;
}
// Version gate before anything else is interpreted.
if (decoded['zap'] != zapProtocolVersion) {
_emitError(
emit,
code: 'version',
message:
'unsupported ZAP protocol version '
'"${decoded['zap']}" (this host speaks $zapProtocolVersion)',
inReplyTo: decoded['id'] is String ? decoded['id'] as String : null,
);
return;
}
// Structural validation (schema).
final validation = ZapValidator.validate(decoded);
if (!validation.ok) {
_emitError(
emit,
code: 'schema',
message:
'message rejected: ${validation.issues.length} schema '
'violation(s)',
inReplyTo: decoded['id'] is String ? decoded['id'] as String : null,
details: [for (final i in validation.issues) i.toString()],
);
return;
}
final type = decoded['type'] as String;
// Direction gate: only missions and checkpoint save/restore come in.
if (type != 'mission') {
final kind = decoded['kind'];
final inboundKind = kind == 'save' || kind == 'restore';
if (type != 'checkpoint' || !inboundKind) {
_emitError(
emit,
code: 'direction',
message:
'"$type${type == 'checkpoint' ? ' ($kind)' : ''}" is a '
'host-to-agent message; only mission and checkpoint '
'save/restore may be sent inbound',
inReplyTo: decoded['id'] as String?,
);
return;
}
await _handleCheckpoint(decoded, emit);
return;
}
await _handleMission(decoded, emit);
}