Two count vocabularies flow through the stream and must NOT both be summed:
stage.complete / run.complete carry the authoritative applied quartet
{processed,succeeded,failed,skipped} for a finished stage/run → these aggregate.
records.batch.complete (and progress.heartbeat) carry a fetched / in-flight
processed count for live per-batch progress only. The same records are reported
again — applied — by the stage rollup, so summing batch counts double-counts every
record (a 56-record sync would report processed:112). These do NOT aggregate.
The emitter and the reader both call this so a re-derived count always matches the
emitter's running aggregate. Centralizing the rule here keeps the two in lock-step.
Whether an event's
countsfeed the run-level APPLIED aggregate (IntegrationRunResult.aggregateCounts / IntegrationRunSnapshot.counts).Two count vocabularies flow through the stream and must NOT both be summed:
stage.complete/run.completecarry the authoritative applied quartet {processed,succeeded,failed,skipped} for a finished stage/run → these aggregate.records.batch.complete(andprogress.heartbeat) carry a fetched / in-flightprocessedcount for live per-batch progress only. The same records are reported again — applied — by the stage rollup, so summing batch counts double-counts every record (a 56-record sync would report processed:112). These do NOT aggregate.The emitter and the reader both call this so a re-derived count always matches the emitter's running aggregate. Centralizing the rule here keeps the two in lock-step.