diff --git a/CHANGELOG.md b/CHANGELOG.md index 14c8a71..3a0ca2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,13 @@ The isolated `trainlog-program-recalage-v1` candidate adds an explicit preview-and-confirm cascade for unfinished Program sessions. It retains source and actual dates, stores absolute current dates and ceded-slot state, and -selects a separate Programs V2 Android projection by capability. This candidate -is not merged into the stable installation or deployed to Android. -Rollout packaging now rejects a native build without the embedded Web frontend. +selects a separate Programs V2 Android projection by capability. The source +candidate remains isolated from the stable checkout while its private desktop +and compatible Android build are installed for the coordinated rollout. +Rollout packaging rejects a native build without the embedded Web frontend. +The private rollout preserves parsed V1 Program number types during Android V2 +upgrade and raises the bounded local MTP operation budget to 90 seconds and +retained outgoing generation capacity to 16 after interrupted exchanges. Development opened after the stable v0.1.6 release. This line is restricted to observed bug fixes, regression fixes, directly related small UI corrections, diff --git a/docs/current_state.md b/docs/current_state.md index 66ad039..78c6622 100644 --- a/docs/current_state.md +++ b/docs/current_state.md @@ -59,7 +59,7 @@ callbacks complete before the mutable generation reference is published; the reference and Android ACK use the same confirmation. Bounded diagnostic logs contain only run/generation identities, artifact names, phases and results. The desktop worker performs lightweight local correlation checks continuously -but opens a full MTP pull no more than once every three seconds. A 30-second +but opens a full MTP pull no more than once every three seconds. A 90-second individual adapter budget is distinct from the overall conversation deadline, so an absent correlated generation reports a protocol wait timeout while an actually blocked adapter reports `transport_timeout`. diff --git a/docs/sync_exchange.md b/docs/sync_exchange.md index 4a86940..023a1ad 100644 --- a/docs/sync_exchange.md +++ b/docs/sync_exchange.md @@ -1112,7 +1112,7 @@ named by that validated reference. It does not recursively mirror retained Android generations, desktop publications, or unrelated exchange history. Local correlation is checked every 50 ms without USB work; complete MTP pulls are throttled to a three-second cadence. Each MTP operation has its own -30-second ceiling while the conversation retains its configured global +90-second ceiling while the conversation retains its configured global deadline. Exhausting the global deadline while waiting for a correlated reference reports `timeout waiting for correlated android-generation-v1.json`; only an adapter that exceeds its independent operation budget reports diff --git a/docs/sync_rollout_readiness.md b/docs/sync_rollout_readiness.md index 4089b4b..5c0e454 100644 --- a/docs/sync_rollout_readiness.md +++ b/docs/sync_rollout_readiness.md @@ -110,7 +110,7 @@ These are unsigned development evidence, not deployment authorization. Do not delete an unacknowledged generation, the last acknowledged generation, its previous valid generation, or causal evidence. An ACK timeout is unresolved, -not rollback. Eight retained outgoing generations exhaust admission and require +not rollback. Sixteen retained outgoing generations exhaust admission and require operator diagnosis; this tranche adds no tombstone or evidence garbage collection. A migrated database is recovered from its verified backup, never by assuming an older binary reverses its migration. diff --git a/tests/test_sync_error_classification.py b/tests/test_sync_error_classification.py index b7db830..aaec5b6 100644 --- a/tests/test_sync_error_classification.py +++ b/tests/test_sync_error_classification.py @@ -128,21 +128,25 @@ class SyncErrorClassificationTest(unittest.TestCase): ) def test_transport_timeout_has_a_stable_code_and_bounded_diagnostic(self): - timeout = subprocess.TimeoutExpired(["adapter", "push"], 30) + operation_budget = sync_peer_worker.MTP_OPERATION_TIMEOUT_SECONDS + timeout = subprocess.TimeoutExpired( + ["adapter", "push"], operation_budget, + ) with tempfile.TemporaryDirectory() as directory: transport = Path(directory) / "transport" transport.mkdir() with mock.patch.object(sync_peer_worker.subprocess, "run", side_effect=timeout): with self.assertRaisesRegex( RuntimeError, - r"^transport_timeout: MTP push did not finish within 30 seconds$", + rf"^transport_timeout: MTP push did not finish within " + rf"{operation_budget:g} seconds$", ) as caught: sync_peer_worker.run_adapter( Path("/adapter"), "push", "peer_00000000-0000-4000-8000-000000000001", transport, - sync_peer_worker.time.monotonic() + 60, + sync_peer_worker.time.monotonic() + operation_budget + 10, ) self.assertEqual(sync_orchestrator.failure_code(caught.exception), "transport_timeout") diff --git a/tests/test_sync_peer_worker.py b/tests/test_sync_peer_worker.py index e83e6cf..b86f47a 100644 --- a/tests/test_sync_peer_worker.py +++ b/tests/test_sync_peer_worker.py @@ -58,7 +58,8 @@ class SyncPeerWorkerTest(unittest.TestCase): with mock.patch.object(worker.subprocess, "run", return_value=completed) as run: worker.run_adapter( Path("/fixed/adapter"), "pull", "peer_fixed", - self.transport, 100.0, clock=lambda: 50.0, + self.transport, 50.0 + worker.MTP_OPERATION_TIMEOUT_SECONDS + 10.0, + clock=lambda: 50.0, ) self.assertEqual(worker.MTP_OPERATION_TIMEOUT_SECONDS, run.call_args.kwargs["timeout"]) @@ -112,7 +113,8 @@ class SyncPeerWorkerTest(unittest.TestCase): with self.assertRaisesRegex(RuntimeError, "transport_timeout"): worker.run_adapter( Path("/fixed/adapter"), "pull", "peer_fixed", - self.transport, 100.0, clock=lambda: 50.0, + self.transport, 50.0 + worker.MTP_OPERATION_TIMEOUT_SECONDS + 10.0, + clock=lambda: 50.0, ) def test_delayed_correlated_generation_survives_multiple_polls(self): diff --git a/tools/sync_generation_exchange.py b/tools/sync_generation_exchange.py index c6a657f..86056d3 100644 --- a/tools/sync_generation_exchange.py +++ b/tools/sync_generation_exchange.py @@ -51,7 +51,11 @@ MAX_GENERATION = 256 * 1024 * 1024 MAX_NAME = 64 MAX_PATH = 240 MAX_DIAGNOSTIC = 1024 -MAX_RETAINED_PER_PEER = 8 +# WHY: interrupted MTP publications can leave immutable generations awaiting +# an ACK alongside the two acknowledged lineage members kept for recovery. +# CONTRACT: admission remains bounded and never erases unresolved generations. +# INVARIANT: captured, published and waiting generations still consume a slot. +MAX_RETAINED_PER_PEER = 16 ID = re.compile(r"^(?:gen|peer|sy)_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") HEX = re.compile(r"^[0-9a-f]{64}$") diff --git a/tools/sync_peer_worker.py b/tools/sync_peer_worker.py index f21af32..09ade4b 100644 --- a/tools/sync_peer_worker.py +++ b/tools/sync_peer_worker.py @@ -37,7 +37,11 @@ CAPS = { ACK_RECOVERY_CAP = "generation-ack-recovery-v1" -MTP_OPERATION_TIMEOUT_SECONDS = 30.0 +# WHY: a verified personal phone can need more than 30 seconds to publish an +# immutable Programs generation over MTP. CONTRACT: this bounds each adapter +# call independently of the configured overall conversation deadline. +# INVARIANT: an adapter overrun remains an ambiguous transport failure. +MTP_OPERATION_TIMEOUT_SECONDS = 90.0 MTP_POLL_INTERVAL_SECONDS = 3.0