Addresses santiagomed's review on #68930:
- api.py: drop created_at_msec from chat_message_event.fields — the
endpoint 400s on unknown fields, so every poll failed. Use the
documented created_at instead, and document the valid field set.
- adapter.py: replace the in-memory backlog/dedup scheme with a
persisted per-conversation cursor (~/.hermes/xchat/cursors.json).
Fixes three defects at once: re-replies to old messages after
dedup prune eviction, dropped bursts >50 events between polls
(the poll now pages back to the cursor), and messages received
while the gateway was down being swallowed as backlog on restart.
- adapter.py/crypto.py: dispatch MessageEdit events — the feed can
return an edited message only as the edit event, which previously
made it invisible to the bot forever.
- adapter.py: _standalone_send now fetches participants' public keys
and calls set_signing_keys BEFORE decrypt_batch, so KeyChange
verification can pass and the conversation key actually seeds
against the real SDK.
- cli.py: refuse to regenerate over an existing private-key blob when
the registration marker is missing/corrupt (no forward secrecy —
overwriting permanently kills every conversation); --force now backs
the old blob up first, and the register subcommand accepts --force too.
- cli.py: write the key blob via os.open(..., 0o600) — no world-
readable window between write_text and chmod.
- docs: XCHAT_PRIVATE_KEYS_B64 compromise warning (messaging page +
env-var reference); inbound section updated for cursors/edits.
- tests: API-layer coverage (401 refresh + rotation persistence,
single-retry guard, proactive expiry refresh, 429 reset, documented
event fields, path hyphenation), a StrictCrypto fake that refuses
keys until set_signing_keys is called (would have caught the
standalone-send bug), cursor restart/burst/edit tests, and CLI
blob-guard/0600 tests.