Do not trust the engine's count as the pace

The second probe run read TxBufLen as 0 one millisecond and again fifty
milliseconds after twenty-one characters had been pushed, while those characters
were already going out: the first of them was decoded back off the air 420 ms
later. The number is right when it is a second old and wrong when it is fresh,
so it runs behind the engine.

A pump that fed on it would have handed over the whole message in half a second
and put all of it beyond reach, which is worse than the clock it replaced. So
the clock is the pace again, and the count is a check on it: the engine is never
given more than Lead + Slack characters however fast the clock says to feed, and
a message ends when the count and the clock estimate both say it has. Characters
fed since the last answer are added to it, so a stale answer cannot be spent
twice.

The engine is set to Character out: ABCD with no space after it went out at
once. Word out would have held it, so the holding case stays, but it now needs
the count to be neither going down nor being added to, since an engine kept
exactly at the cap has a count that does not move either.

The probe writes down every reading for two seconds after a push and does not
believe an empty one in that window, which is what the run needed to measure the
lag and did not do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtspmWmS7f8kUvcyaHpRWZ
This commit is contained in:
2026-09-01 23:13:28 +00:00
parent 4118cd6d33
commit 5370ad3f6d
4 changed files with 142 additions and 192 deletions

View File

@@ -41,6 +41,11 @@ public sealed class EngineProbe
/// A count that has not moved for this long is not going to move.
private static readonly TimeSpan Still = TimeSpan.FromSeconds(5);
/// How long after a push the count is written down every time it is read,
/// and an empty answer is not believed. The count runs behind the engine,
/// and this is where the report says by how much.
private static readonly TimeSpan Settling = TimeSpan.FromSeconds(2);
/// How long the engine is given to key up and to drop again.
private static readonly TimeSpan Keying = TimeSpan.FromSeconds(3);
@@ -126,6 +131,7 @@ public sealed class EngineProbe
await KeyAsync(cancellation).ConfigureAwait(false);
await TypeAsync(Message, cancellation).ConfigureAwait(false);
log.Write($"pushed {Message.Length} characters");
log.Write("every reading for the next two seconds: the first one that is not 0 says how far behind the count is");
await DrainAsync(cancellation).ConfigureAwait(false);
log.Write($"received while transmitting: \"{TakeReceived()}\"");
await UnkeyAsync(cancellation).ConfigureAwait(false);
@@ -173,20 +179,22 @@ public sealed class EngineProbe
/// registered, not that the message has gone.
private async Task DrainAsync(CancellationToken cancellation)
{
DateTime giveUp = DateTime.UtcNow + Patience;
DateTime moved = DateTime.UtcNow;
DateTime started = DateTime.UtcNow;
DateTime giveUp = started + Patience;
DateTime moved = started;
int last = int.MinValue;
bool wasEmpty = false;
while (DateTime.UtcNow < giveUp)
{
int left = await AskAsync("", cancellation).ConfigureAwait(false);
if (left != last)
bool settling = DateTime.UtcNow - started < Settling;
if (left != last || settling)
{
log.Write($"TxBufLen: {Answer(left)}");
moved = left != last ? DateTime.UtcNow : moved;
last = left;
moved = DateTime.UtcNow;
}
if (left == 0 && wasEmpty)
if (left == 0 && wasEmpty && !settling)
{
return;
}