fix(marmot): persist group state after inline PrivateMessage commit

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
This commit is contained in:
Claude
2026-04-22 07:31:20 +00:00
parent 0f3d4e04f2
commit d0fb8ac788
@@ -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
}