perf(chat): sends re-read by origin, chat scans under the scan budget

A z_sendmany submit set off a burst of wallet scans before anything had
changed. Ops now re-read the wallet when they finish, by who started them:
the user's sends and dialog ops get a re-read plus one forced balance once
none of their ops is pending; client auto-shield re-reads unforced; chat
messages and splits refresh only the chat note view. A pending send no
longer forces scans, and chat txids no longer enter the tracked-send set.

Chat ingest skips stored messages before decrypting and caches
undecryptable ones per identity. The inbox scan runs when getwalletinfo's
txcount changed (a cheap probe, at most 2% duty) or a cost-scaled safety
net is due; the note-buffer scan runs on need. Both are admitted by the
scan budget, on a steady clock. The chat payer comes from a per-address
view of verified notes, with debits re-applied across in-flight scans;
splits are sized to one payer and only when they can refill the buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 11:06:34 -05:00
co-authored by Claude Opus 5.5
parent f86cc4d6b7
commit b29a09eaa0
15 changed files with 1037 additions and 177 deletions
+16
View File
@@ -311,6 +311,16 @@ public:
bool anySuccess = false;
};
// Who started a tracked async operation, i.e. what its completion re-reads (App::markWalletMutated).
// Absent from the App's origin map = User.
enum class OperationOrigin : std::uint8_t { User, Auto, Chat };
struct OperationBatchPlan {
bool userSucceeded = false; // >= 1 User op succeeded: re-read the wallet, force the balance
bool autoSucceeded = false; // >= 1 Auto op (client auto-shield) succeeded: re-read, unforced
bool walletTerminal = false; // >= 1 non-chat op finished (any outcome) or went stale
bool chatTerminal = false; // >= 1 chat op finished or went stale: refresh the chat note view
};
struct TransactionCacheUpdate {
TransactionViewCache& viewTxCache;
std::unordered_set<std::string>& sendTxids;
@@ -435,6 +445,12 @@ public:
// retained. Built explicitly, because a one-element braced list of a json ({ids}) collapses to the
// inner array, which the daemon rejects ("JSON value is not an array as expected").
static std::optional<nlohmann::json> buildOperationFilterParams(const std::vector<std::string>& opids);
// Classify a poll's terminal ops by origin (absent from the map = User).
static OperationBatchPlan planOperationBatch(const OperationStatusPollResult& parsed,
const std::unordered_map<std::string, OperationOrigin>& origins);
// Any pending id is a User op (absent from the map = User).
static bool anyUserOperationPending(const std::vector<std::string>& pendingOpids,
const std::unordered_map<std::string, OperationOrigin>& origins);
struct DaemonRescanScan {
bool foundRescan = false;