+ {t.how.title} +
+{t.how.sub}
++ {t.how.proof} +
+- > {
- val pubkey = client.pubkey()
- ?: return Result.failure(IllegalStateException("backend pubkey unavailable"))
+ if (!backendPubkeyHex.matches(Regex("[0-9a-fA-F]{64}"))) {
+ return Result.failure(IllegalStateException("backend public-key trust anchor missing from build"))
+ }
val verified = mutableListOf
- >
+
+ @Query(
+ "UPDATE neighbor_estimates SET state = CASE " +
+ "WHEN :now - last_seen_at >= :lostAfter THEN 2 " +
+ "WHEN :now - last_seen_at >= :degradedAfter THEN 1 " +
+ "ELSE 0 END",
+ )
+ suspend fun refreshNeighborStates(now: Long, degradedAfter: Long, lostAfter: Long)
+
+ // ── Control frames: persistent replay guard + acted-on trust state ─────────
+
+ /**
+ * Admit a freshly-verified control frame exactly once per signature. Returns
+ * the row id (>0) iff newly inserted; -1 (IGNORE conflict) on a replay. This
+ * is the persistent complement to [DedupeCache] — the gate for acting on /
+ * relaying a control frame across reboots.
+ */
+ @Insert(onConflict = OnConflictStrategy.IGNORE)
+ suspend fun insertControlReplay(entity: ControlReplayEntity): Long
+
+ @Query("SELECT EXISTS(SELECT 1 FROM control_replay WHERE signature_hex = :signatureHex)")
+ suspend fun hasControlReplay(signatureHex: String): Boolean
+
+ @Query("DELETE FROM control_replay WHERE expires_at < :now")
+ suspend fun pruneControlReplay(now: Long): Int
+
+ @Query("SELECT COUNT(*) FROM control_replay")
+ suspend fun countControlReplay(): Int
+
+ /**
+ * Upsert the trust state for one of this device's own SOS frames. One row per
+ * `(node_id, msg_id)`; a terminal upsert is the local authority to stop TX.
+ */
+ @Insert(onConflict = OnConflictStrategy.REPLACE)
+ suspend fun upsertControlTrust(entity: ControlTrustEntity)
+
+ @Query("SELECT * FROM control_trust WHERE node_id = :nodeId AND msg_id = :msgId")
+ suspend fun getControlTrust(nodeId: String, msgId: Int): ControlTrustEntity?
+
+ @Query("SELECT * FROM control_trust WHERE node_id = :nodeId AND msg_id = :msgId")
+ fun observeControlTrust(nodeId: String, msgId: Int): Flow
- >
+ @Query("SELECT * FROM chat_fragments WHERE sender_node_id = :sender AND message_id = :messageId ORDER BY fragment_index ASC")
+ suspend fun chatFragments(sender: String, messageId: Long): List
- > = dao.observeNeighborEstimates()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+
+ val chatMessages: StateFlow
- > = GuacamayaDatabase.get(app).chatDao().observeChatMessages()
+ .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
+ val radioPolicy = ChatRuntime.policy
+ val chatActive = ChatRuntime.active
+
private val _identity = MutableStateFlow
- > = throw NotImplementedError()
@@ -43,6 +71,23 @@ class IngestRepositoryTest {
override suspend fun latestHelpFramesPerNode(limit: Int): List
- > = throw NotImplementedError()
+ override suspend fun refreshNeighborStates(now: Long, degradedAfter: Long, lostAfter: Long) = throw NotImplementedError()
+
+ // ── Control frames: not touched by the uploader. ──────────────────────
+ override suspend fun insertControlReplay(entity: ControlReplayEntity): Long = throw NotImplementedError()
+ override suspend fun hasControlReplay(signatureHex: String): Boolean = throw NotImplementedError()
+ override suspend fun pruneControlReplay(now: Long): Int = throw NotImplementedError()
+ override suspend fun countControlReplay(): Int = throw NotImplementedError()
+ override suspend fun upsertControlTrust(entity: ControlTrustEntity) = throw NotImplementedError()
+ override suspend fun getControlTrust(nodeId: String, msgId: Int): ControlTrustEntity? = throw NotImplementedError()
+ override fun observeControlTrust(nodeId: String, msgId: Int): Flow
- > = throw NotImplementedError()
+ override suspend fun refreshNeighborStates(now: Long, degradedAfter: Long, lostAfter: Long) = throw NotImplementedError()
+
+ // ── Control frames: in-memory replay guard + trust state. Reused by the
+ // ControlRouter tests (Phase 4). Mirrors Room semantics: IGNORE→-1 on replay,
+ // REPLACE for trust upsert. ───────────────────────────────────────────────
+ private val controlReplay = linkedMapOf
- >()
- override suspend fun upload(frames: List