From d0fb8ac7880305f3461bf351623e6aaa25fc1424 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Apr 2026 07:31:20 +0000 Subject: [PATCH] fix(marmot): persist group state after inline PrivateMessage commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PrivateMessage commits apply their Commit body inline inside `MlsGroup.decrypt`, so the epoch advance + extension replace happen in `MlsGroupManager.decrypt` — not in `processCommit`. The old `decrypt` path never called `persistGroup`, so amy's in-memory promotion (e.g. test 08 "B promotes A") looked correct for the current CLI invocation but the NEXT `amy marmot …` command reloaded the pre-commit extensions from disk and refused the follow-up rename with "MIP-01: only admins may update group extensions". Detect epoch advance + COMMIT result inside `decrypt`, then push the pre-commit exporter into the retained-epoch window and persist the group so CLI reloads see the post-commit state. https://claude.ai/code/session_1469d7f4-bb66-4ffa-a44d-1dfa4b526484 --- .../quartz/marmot/mls/group/MlsGroupManager.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/group/MlsGroupManager.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/group/MlsGroupManager.kt index 9822bdbe1..8cc77f19f 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/group/MlsGroupManager.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/marmot/mls/group/MlsGroupManager.kt @@ -341,9 +341,22 @@ class MlsGroupManager( // hiding the real bug. Capture the original throwable, try // retained epochs as a fallback, and re-raise the captured one // if nothing decrypts. + val retainedBefore = group.retainedSecrets() + val preEpoch = group.epoch val currentFailure: Throwable? = try { - return@withLock group.decrypt(messageBytes) + val result = group.decrypt(messageBytes) + // PrivateMessage commits apply inline through + // `MlsGroup.decrypt` → `processCommit`; the epoch advances + // in memory but the CLI reopens a fresh Context on every + // command, so we MUST persist here or reloaded state + // silently reverts to the pre-commit extensions (including + // admin list). + if (result.contentType == com.vitorpamplona.quartz.marmot.mls.framing.ContentType.COMMIT && group.epoch != preEpoch) { + pushRetainedEpoch(nostrGroupId, retainedBefore) + persistGroup(nostrGroupId) + } + return@withLock result } catch (t: Throwable) { t }