acpmux: a tags or rules change logs its line before its metadata (R-DURABILITY S3b)

Merged
LA
1d ago
+129-4

Summary

Stage S3b of the R-DURABILITY rewrite (design: handoff/rewrites/R-DURABILITY/DESIGN.md 3.3e, "C2 ... revised 2026-11-04"). Stacked on the S2 follow-ups PR, which is stacked on #35483 (S3a).

E6: a chat metadata change that also logs a line (tags, renamed, rules, policy, mode) must never be, after a crash, in the chat's metadata without its line in the log. With the event journal (S2, S3a), records survive a crash as a prefix in append order. So the change is safe when its line reaches the store before any metadata image that holds it. renamed and policy already did this; tags and rules saved their metadata first. (mode mostly did; see the review notes below.) This PR moves their line first. Their reply still waits for the save (and, with the journal on, for its record: S2c).

Why not one combined record (the design's first plan): an event-log line goes to the log file before its record (S2's log-first append), and the repair keeps a line only the log holds. So after a kill the log can hold the line while no record of it is durable, and one record with both the line and the image would not make them atomic in the log. It would also race MetaWriter's coalesced copies.

What changes

  • hub/tags.rs set_tags_answered and hub/mod.rs set_rules_answered: the line, then save_meta_answered.

Known gaps

  • The reverse is still possible in one group-commit window: the line without the metadata, after a crash. The change's reply waits for its metadata record, so that change was never acknowledged.
  • A MetaWriter copy that another thread takes between the change and its line, appended before the line, then a crash in the next group commit (DESIGN.md 3.3e).
  • The 3.3d limit (a change in memory before its record, seen by another connection's reply) stays (DESIGN.md 3.3d).
  • From the review at 71d7e096cba (SHIP, comment 6638442340; P3, follow-ups owned by R-DURABILITY, written into DESIGN.md 3.3e): write_mode_state saves before the mode line when cmux's mode mark clears; a chat's first append after a start (catch_up_locked) can save an image that already holds the new tags before the tags line; with the journal on, a watcher can see the tags event one group commit before its metadata record is durable (the gate holds the event only to the Ack LSN read when the line is queued); other sites save, then log (seen.rs, work_below.rs, turns.rs model_request, tap.rs, watch.rs; transfer_out.rs on purpose).

Testing

acpmux hub::tags::meta_line_order_tests (a recording store checks the order of the store calls):

  • a_tag_change_logs_its_line_before_its_metadata, a_rules_change_logs_its_line_before_its_metadata: red at 7a4f02a56f3 (the tests before the fix), owned Mac aws-m4pro-7, fleet 88309e91: tags ["save:tagged", "line:tags"]; rules ["line:permission_chat_allowance", "save:ruled", "line:rules"].
  • a_rename_logs_its_line_before_its_metadata: a guard, green before and after.

Merged down since the SHIP at 71d7e096cba (no change to this PR's own files): the S2 follow-ups fixes (the idle checkpointer race 5279af4bbf7, the repair file 3b7007c175d), the S3a fixes (0d246364d90, N1 fc83b326fba) and feat 7427100d5d0 (with #35845). Head 7fc15b776ef: fleet 1a6d2f28 (owned Mac aws-m4pro-7), journal_ 350 of 350; fleet ea9d2c52 (Linux, hq run 41907479976), hub:: 1139 of 1139 (acpmux 1125, cmux-remote 1, cmux-tui 13). fmt at 66e1df5ec5e: fleet 9232d441, green. The 50 runs of a_failed_log_sync_freezes_the_checkpoint at 71d7e096cba had 1 failure, the idle checkpointer race (#35562 has the analysis); 0 of 50 at the fix.

Green at 71d7e096cba: fleet 25d8970a (owned Mac aws-m4pro-3), the 3 order tests pass; fleet 0b984386 (owned Mac aws-m4pro-5), hub:: 1083 of 1084. The one failure, hub::shutdown_tests::a_cut_turn_of_agent_messages_puts_none_of_them_back, timed out once in the full run (shutdown_tests.rs:237, Elapsed) and passed on its rerun; it uses no tags or rules.

Observed / Not run

  • Observed: the fleet steps above.
  • Not run: policy and mode in this test (their paths need a running agent); they write the line first by reading.

Merged down after #35562 landed (head 6a5a6d138c9)

bf2f48d677a merges feat 07c879aab80, which has #35562 as one squash (0b9e1668a7b). It conflicted in 4 store files (journal_store.rs, journal_store/meta.rs, journal_store/repair.rs, journal_store_tests.rs). All 4 are resolved to feat's copy, because this PR never changed a store file of its own. 6a5a6d138c9 is a clean merge-main of feat ecfe8d94e9d. Against feat, the head differs only in this PR's 3 hub files (hub/meta_line_order_tests.rs, hub/mod.rs, hub/tags.rs; 129 lines added, 4 removed).

Green at 6a5a6d138c9:

  • macOS journal_: 355 of 355 (fleet 172c6c27, aws-m4pro-7).
  • macOS meta_line_order: 3 of 3 (fleet 8a6b3a10, aws-m4pro-8).
  • Linux all hub:: with fmt and clippy -D warnings: 1139 of 1139 (fleet da5b97a1, Blacksmith; hq run 41935007261).
  • Linux all store:: with fmt and clippy -D warnings: 356 of 356 (fleet 51e7e557, Blacksmith; hq run 41935006187).

Changelog

none

Proof

No capture: a daemon change with no UI.


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Makes tags and rules changes log their event line before saving their metadata, closing a durability gap: after a crash, a change could previously survive in the chat's metadata without its line in the log. With the event journal, records survive as a prefix in append order, so logging first guarantees the line is durable before any metadata image holds the change. renamed, policy, and mode already did this; only tags and rules saved first, and their replies still wait for the save.

Adds meta_line_order_tests.rs, a recording store that asserts the store call order for tags and rules changes plus a rename guard.