From 457abc0a5181794bfe721f5a0ae4591614c5f6ae Mon Sep 17 00:00:00 2001 From: fy59 Date: Fri, 18 Sep 2026 23:25:43 +0200 Subject: [PATCH] Normalize recoverable rejection acknowledgements --- tests/test_sync_error_classification.py | 12 ++++++++---- tools/sync_generation_exchange.py | 5 +++++ tools/sync_peer_worker.py | 10 +++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/test_sync_error_classification.py b/tests/test_sync_error_classification.py index 1645edd..391a8f0 100644 --- a/tests/test_sync_error_classification.py +++ b/tests/test_sync_error_classification.py @@ -23,14 +23,18 @@ class SyncErrorClassificationTest(unittest.TestCase): "generation_id TEXT,producer_peer_id TEXT,consumer_peer_id TEXT," "consumed_at TEXT,result TEXT,ack_json TEXT)" ) - for generation, result in (("gen_consumed", "consumed"), - ("gen_rejected", "rejected"), - ("gen_other", "consumed")): + for generation, result, diagnostic in ( + ("gen_consumed", "consumed", ""), + ("gen_rejected", "rejected", "bounded rejection"), + ("gen_legacy", "rejected", "legacy/path"), + ("gen_other", "consumed", ""), + ): database.execute( "INSERT INTO sync_consumed_generations VALUES(?,?,?,?,?,?)", (generation, "android" if generation != "gen_other" else "other", "desktop", generation, result, - json.dumps({"generation_id": generation, "result": result})), + json.dumps({"generation_id": generation, "result": result, + "diagnostic": diagnostic})), ) acknowledgements = sync_peer_worker.recovery_acknowledgements( diff --git a/tools/sync_generation_exchange.py b/tools/sync_generation_exchange.py index efb4f50..65e3f16 100644 --- a/tools/sync_generation_exchange.py +++ b/tools/sync_generation_exchange.py @@ -566,6 +566,11 @@ def record_consumed(db: sqlite3.Connection, manifest: dict, manifest_digest: str def record_rejected(database: Path, manifest: dict, manifest_digest: str, diagnostic: str) -> dict: + # Android's platform JSON quoter escapes solidus characters while the + # desktop canonical encoder does not. Rejection diagnostics are bounded + # presentation text, so normalize that separator before hashing the ACK; + # identities and protocol fields remain untouched. + diagnostic = diagnostic.replace("/", ":") diagnostic = diagnostic.encode("utf-8")[:MAX_DIAGNOSTIC].decode("utf-8", "ignore") with closing(connect_database(database)) as db: require_schema(db); db.execute("BEGIN IMMEDIATE") diff --git a/tools/sync_peer_worker.py b/tools/sync_peer_worker.py index 4f63522..c1f3dc5 100644 --- a/tools/sync_peer_worker.py +++ b/tools/sync_peer_worker.py @@ -75,7 +75,15 @@ def recovery_acknowledgements(db, producer_peer_id: str, consumer_peer_id: str) "ORDER BY consumed_at,generation_id LIMIT 32", (producer_peer_id, consumer_peer_id), ).fetchall() - return [json.loads(row[0]) for row in rows] + acknowledgements = [json.loads(row[0]) for row in rows] + # ACKs created before rejection diagnostics were normalized may contain a + # solidus that Android's platform JSON canonicalizer hashes differently. + # They remain durable evidence but cannot close a peer row, so do not let + # one legacy object block recovery of later compatible conversations. + return [ + value for value in acknowledgements + if value.get("result") != "rejected" or "/" not in value.get("diagnostic", "") + ] def run_adapter(