Nice example. Your approach is basically correct, but misses one important thing arising because of the join.
(1) First take the case of Manifold 8.
The join is
FROM A, B
WHERE Contains(A.ID, B.ID)
or more explicitly (same logic)
FROM A INNER JOIN B
ON Contains(A.ID, B.ID)
What happens if, for some given record in A, there are more than records in B whose geometry meets the join condition, that is, if the geom in A contains more than one geoms in B?
Then that record from A will be listed more than once in the joined table, each time against a different record from B.
(It will be the same the other way around, i.e. if for a given record in B, its geom is contained by the geom of more than one record in A. But that does not make the difference here.)
What will happen to the UPDATE in this case? In 8, any target record in A which is listed multiple times will likewise be updated multiple times, with the last update winning. That "works" but is normally an unpredictable result.
(2) Now look at 9. A similar join, with slightly different syntax (and different effective tolerance).
FROM A INNER JOIN B
On GeomContains(A.[Geom], B.[Geom], 0)
Now what happens? At first the same thing.
Some record(s) from A might be matched against more than one record from B, and if that happens, that or those record(s) will again be listed more than once.
The first important point is that whether this occurs depends on the data. Until the actual data is examined, it remains a possibility. Manifold compiles a query before source data is examined, so it must allow for this possibility: it must assume that some record(s) may be multiply matched in the join.
This means that it can't assume that the BTREE from the left table will remain valid after the join. Some records might be matched multiple times, some might not be matched at all, meaning a BTREE structure would fail.
It therefore pre-emptively converts the BTREE from the left table (and from the right table, but this is not the present point) to a BTREEDUPNULL index, to allow for both possibilities.
That is why you can't UPDATE this joined table in 9. The index on the joined table is no longer a BTREE, and moreover is no longer directly inherited from the (left) source table. There is no longer a one-to-one match between records. (You can compare this to the result of an aggregate, which also breaks the one-to-one link.)
Why can Manifold 8 make this "just work", while Manifold 9 cannot? Well, remember that 8 is doing something logically unpredictable (in advance of knowing the exact data), while 9 insists on predictability (in all cases). 9 cares, 8 does not.
Next step? How to make sure that each record from the left table can only be listed once, so that its BTREE index can be (and will be) strictly preserved in the joined table?