@botbuddy/cli 1.29.2 → 1.29.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/wait-core.mjs CHANGED
@@ -1023,6 +1023,7 @@ export async function runWaitLoop({
1023
1023
  backoff = (attempt) => nextBackoff(attempt),
1024
1024
  sleep = null,
1025
1025
  onFrame = null,
1026
+ startedAt: originalStartedAt = null,
1026
1027
  // BOT-1259: the wait session's server-resolved tenant (from the register echo). A
1027
1028
  // tenanted spine signal only matches a session resolved to that tenant; `null` (the
1028
1029
  // session's tenant could not be resolved) fails closed, and `undefined` (the caller
@@ -1042,6 +1043,9 @@ export async function runWaitLoop({
1042
1043
  // clear on recovery). Injected so the CLI wires the real freshness query and a
1043
1044
  // test drives it clock-controlled; omitted (null) = no probing.
1044
1045
  feedLagProbe = null,
1046
+ // On an expired recovery, replay only durable rows created before this original
1047
+ // deadline; the caller supplies a short transport grace via deadlineMs.
1048
+ replayOnlyThroughMs = null,
1045
1049
  // BOT-1148: after a beacon-driven source_stale:true transition on a HOST-PINNED
1046
1050
  // capacity wait, wait this long for a recovery (source_stale:false) before exiting
1047
1051
  // capacity_source_stale. Absorbs threshold flap on the client (the server latch is
@@ -1057,13 +1061,28 @@ export async function runWaitLoop({
1057
1061
  // frame. 90s = 60s cron + margin.
1058
1062
  beaconConfirmMs = 90_000,
1059
1063
  }) {
1060
- const startedAt = nowIso();
1064
+ const startedAt = typeof originalStartedAt === "string" && Number.isFinite(Date.parse(originalStartedAt))
1065
+ ? originalStartedAt : nowIso();
1061
1066
  const seen = new Set();
1062
1067
  // An omitted --since arms from the current high-water mark (live only): the
1063
1068
  // relay must NOT replay history, or a brand-new wait would immediately match a
1064
1069
  // long-past lock release. Only an explicit --since replays (cursor != null).
1065
1070
  const sinceParsed = normalizeSince(since);
1066
1071
  let cursor = sinceParsed.kind === "ok" ? sinceParsed.value : null;
1072
+ // The last cursor whose replay range has been confirmed by replay_complete.
1073
+ // Live signals can overtake lower replay rows, so `cursor` is unsafe as a
1074
+ // reconnect cursor until that marker arrives.
1075
+ let replayFrontier = cursor;
1076
+ // A new relay advertises replay_complete in its connected frame. Until that
1077
+ // handshake arrives, retain legacy first-match behavior for an older relay
1078
+ // that cannot certify a replay frontier.
1079
+ let replayPending = false;
1080
+ // A replay can be interleaved with newer live rows. Hold matching rows until
1081
+ // replay_complete confirms the range, then choose the earliest eligible match.
1082
+ // This prevents a terminal receipt from either skipping lower replay matches or
1083
+ // replaying its own match on a chained wait.
1084
+ const pendingReplayMatches = [];
1085
+ let pendingReplaySupersession = null;
1067
1086
  const degraded = [];
1068
1087
  let reconnects = 0;
1069
1088
  let failures = 0;
@@ -1109,13 +1128,34 @@ export async function runWaitLoop({
1109
1128
  degraded,
1110
1129
  matched: extra.matched ?? [],
1111
1130
  state: extra.state ?? { conditions: conditions.map((c) => ({ condition_id: c.id, type: c.type })) },
1112
- nextCursor: cursor,
1131
+ // A live frame can overtake an unconfirmed replay backlog. Never publish
1132
+ // that speculative cursor in a terminal receipt: chained waits must resume
1133
+ // from the confirmed replay frontier until replay_complete promotes it.
1134
+ nextCursor: replayPending ? replayFrontier : cursor,
1113
1135
  error: extra.error ?? null,
1114
1136
  reacquireLock: extra.reacquireLock ?? null,
1115
1137
  });
1116
1138
  return { receipt: truncateReceipt(receipt, receiptMaxBytes), exitCode: extra.exitCode };
1117
1139
  };
1118
1140
 
1141
+ const finalizeSuperseded = async (signal) => {
1142
+ // Re-arm from cursor_start rather than this high sequence. A matching row can
1143
+ // have been allocated earlier and arrive after the supersession on a replay.
1144
+ const cs = signal.payload?.cursor_start;
1145
+ if (cs != null && /^\d+$/.test(String(cs))) cursor = String(cs);
1146
+ const reacquire = conditions
1147
+ .filter((c) => c.type === "lock" && c.params.claim)
1148
+ .map((c) => ({ subtype: c.params.subtype, host: c.params.host, slot: c.params.slot }));
1149
+ return finalize("superseded", {
1150
+ exitCode: EXIT.SUPERSEDED,
1151
+ state: {
1152
+ superseded: true,
1153
+ status: typeof signal.payload?.status === "string" ? signal.payload.status : null,
1154
+ },
1155
+ reacquireLock: reacquire,
1156
+ });
1157
+ };
1158
+
1119
1159
  // Fold the timer conditions and the --timeout cap into one earliest alarm.
1120
1160
  // A timer condition firing is a MATCH (exit 0); the --timeout cap is a
1121
1161
  // TIMEOUT (exit 2). With --any the earliest of the two always wins.
@@ -1131,27 +1171,40 @@ export async function runWaitLoop({
1131
1171
  // (degraded) on an EXISTING exit code (5), never a new numeric code (AC 3).
1132
1172
  for (const c of conditions) {
1133
1173
  if (c.type === "capacity" && c.params.staleGraceSec != null) {
1134
- alarms.push({ kind: "stale", conditionId: c.id, atMs: now() + c.params.staleGraceSec * 1000 });
1174
+ const staleAtMs = Number.isFinite(c.params.staleDeadlineMs)
1175
+ ? c.params.staleDeadlineMs
1176
+ : now() + c.params.staleGraceSec * 1000;
1177
+ alarms.push({ kind: "stale", conditionId: c.id, atMs: staleAtMs });
1135
1178
  }
1136
1179
  }
1137
1180
  const earliest = alarms.length ? alarms.reduce((m, a) => (a.atMs < m.atMs ? a : m)) : null;
1138
-
1139
- const fireAlarm = async () => {
1181
+ // A recovered wait may have alarms that were already due at its original
1182
+ // deadline. Do not put those into the initial race: the durable replay must
1183
+ // establish whether an earlier spine match won. `deadlineMs` is the caller's
1184
+ // short transport grace, so it remains eligible to bound a stuck replay.
1185
+ const replayGraceAlarm = replayOnlyThroughMs == null ? earliest : alarms
1186
+ .filter((alarm) => alarm.atMs > replayOnlyThroughMs)
1187
+ .reduce((m, alarm) => (m == null || alarm.atMs < m.atMs ? alarm : m), null);
1188
+ const restoredAlarm = replayOnlyThroughMs == null ? null : alarms
1189
+ .filter((alarm) => alarm.atMs <= replayOnlyThroughMs)
1190
+ .reduce((m, alarm) => (m == null || alarm.atMs < m.atMs ? alarm : m), null);
1191
+
1192
+ const fireAlarm = async (alarm = earliest) => {
1140
1193
  // The terminal feed-freshness reprobe now lives in finalize() (the single
1141
1194
  // finalization path), so every terminal receipt gets it — not just the alarm path.
1142
- if (!earliest) return null;
1143
- if (earliest.kind === "timeout") return finalize("timeout", { exitCode: EXIT.TIMEOUT });
1144
- if (earliest.kind === "stale") {
1195
+ if (!alarm) return null;
1196
+ if (alarm.kind === "timeout") return finalize("timeout", { exitCode: EXIT.TIMEOUT });
1197
+ if (alarm.kind === "stale") {
1145
1198
  // Capacity source didn't deliver within the grace: degrade truthfully and
1146
1199
  // exit with an error receipt (not a silent hang, not a plain timeout).
1147
1200
  if (!degraded.includes("capacity_source_stale")) degraded.push("capacity_source_stale");
1148
1201
  return finalize("error", { exitCode: EXIT.BACKEND, error: "capacity_source_stale" });
1149
1202
  }
1150
- const condition = conditions.find((c) => c.id === earliest.conditionId);
1203
+ const condition = conditions.find((c) => c.id === alarm.conditionId);
1151
1204
  return finalize("matched", {
1152
1205
  exitCode: EXIT.MATCHED,
1153
1206
  matched: [{
1154
- condition_id: earliest.conditionId,
1207
+ condition_id: alarm.conditionId,
1155
1208
  signal_type: "timer",
1156
1209
  seq: null,
1157
1210
  subject_key: condition?.params?.deadline ?? `+${condition?.params?.duration}s`,
@@ -1160,10 +1213,18 @@ export async function runWaitLoop({
1160
1213
  });
1161
1214
  };
1162
1215
 
1216
+ // The recovery grace bounds one invocation's transport work; it must never
1217
+ // decide the original logical wait. The caller leaves this typed result out
1218
+ // of the durable terminal checkpoint so `bb wait resume` can try the same
1219
+ // replay again from its confirmed cursor.
1220
+ const fireReplayGraceExpired = async () => replayOnlyThroughMs != null
1221
+ ? finalize("error", { exitCode: EXIT.BACKEND, error: "replay_incomplete" })
1222
+ : fireAlarm();
1223
+
1163
1224
  // A single promise that resolves when the earliest alarm passes.
1164
- const deadlinePromise = earliest == null
1225
+ const deadlinePromise = replayGraceAlarm == null
1165
1226
  ? new Promise(() => {}) // no alarm: only a pushed signal can wake us
1166
- : sleepFn(Math.max(0, earliest.atMs - now())).then(() => ({ __deadline: true }));
1227
+ : sleepFn(Math.max(0, replayGraceAlarm.atMs - now())).then(() => ({ __deadline: true }));
1167
1228
 
1168
1229
  // BOT-1147 AC-3: only probe when a linear condition is armed and a probe is
1169
1230
  // wired — every other wait type is unaffected.
@@ -1178,8 +1239,8 @@ export async function runWaitLoop({
1178
1239
  // Fires at most once (a single pre-deadline probe, not a re-arming timer, so an
1179
1240
  // injected instant `sleep` in tests can't spin the race).
1180
1241
  let preDeadlineProbeDone = false;
1181
- const preDeadlineProbePromise = (feedLagActive && earliest != null)
1182
- ? sleepFn(Math.max(0, earliest.atMs - now() - FEED_PROBE_BUDGET_MS)).then(() => ({ __feedProbe: true }))
1242
+ const preDeadlineProbePromise = (feedLagActive && replayGraceAlarm != null)
1243
+ ? sleepFn(Math.max(0, replayGraceAlarm.atMs - now() - FEED_PROBE_BUDGET_MS)).then(() => ({ __feedProbe: true }))
1183
1244
  : null;
1184
1245
  // The specific issue key(s) the armed `linear` conditions target. Threaded into
1185
1246
  // the probe so it scopes freshness to those issues' workspace(s) rather than the
@@ -1294,7 +1355,9 @@ export async function runWaitLoop({
1294
1355
  };
1295
1356
 
1296
1357
  while (true) {
1297
- if (earliest != null && now() >= earliest.atMs) {
1358
+ // Expired recovery must first replay durable pre-deadline rows. A restored
1359
+ // timer or capacity-stale alarm may otherwise win before that replay begins.
1360
+ if (replayOnlyThroughMs == null && earliest != null && now() >= earliest.atMs) {
1298
1361
  return fireAlarm();
1299
1362
  }
1300
1363
 
@@ -1302,9 +1365,16 @@ export async function runWaitLoop({
1302
1365
  // and a recovered one clears — reflected in the terminal receipt / wait_session.
1303
1366
  await probeFeedLag();
1304
1367
 
1368
+ // Every reconnect starts a new replay/live overlap window. A live signal can
1369
+ // overtake the replay backlog, then the transport can die before its marker;
1370
+ // reconnect from the last confirmed frontier so lower replay matches remain
1371
+ // eligible instead of skipping straight to that live signal's cursor.
1372
+ const connectCursor = replayFrontier;
1373
+ const reconnectingIncompleteReplay = replayPending;
1374
+ replayPending = replayOnlyThroughMs != null && connectCursor != null;
1305
1375
  let stream;
1306
1376
  try {
1307
- stream = await connect(cursor);
1377
+ stream = await connect(connectCursor);
1308
1378
  } catch (err) {
1309
1379
  failures += 1;
1310
1380
  degraded.push("backend_unreachable");
@@ -1316,7 +1386,7 @@ export async function runWaitLoop({
1316
1386
  deadlinePromise,
1317
1387
  sleepFn(waitMs).then(() => ({ __retry: true })),
1318
1388
  ]);
1319
- if (raced && raced.__deadline) return fireAlarm();
1389
+ if (raced && raced.__deadline) return fireReplayGraceExpired();
1320
1390
  continue;
1321
1391
  }
1322
1392
 
@@ -1336,9 +1406,7 @@ export async function runWaitLoop({
1336
1406
  if (preDeadlineProbePromise && !preDeadlineProbeDone) racers.push(preDeadlineProbePromise);
1337
1407
  const raced = await Promise.race(racers);
1338
1408
 
1339
- if (raced && raced.__deadline) {
1340
- return fireAlarm();
1341
- }
1409
+ if (raced && raced.__deadline) return fireReplayGraceExpired();
1342
1410
  // BOT-1147 (Codex round-27 P2): the pre-deadline feed probe fired. Fold a
1343
1411
  // mid-wait stall/recovery into `degraded` before the imminent timeout receipt,
1344
1412
  // then keep waiting — pendingNext stays in flight (hoisted), re-raced next
@@ -1375,6 +1443,28 @@ export async function runWaitLoop({
1375
1443
  }
1376
1444
  if (!frame) continue;
1377
1445
 
1446
+ if (frame.event === "connected") {
1447
+ let connected = {};
1448
+ try { connected = JSON.parse(frame.data); } catch { /* legacy relay */ }
1449
+ // Old relays emit connected without this capability and never send the
1450
+ // marker. Preserve their established first-match behavior rather than
1451
+ // buffering a live signal to timeout during a rolling deployment.
1452
+ if (replayOnlyThroughMs != null && connectCursor != null && connected.replay_complete_marker !== true) {
1453
+ return finalize("error", { exitCode: EXIT.BACKEND, error: "replay_marker_unsupported" });
1454
+ }
1455
+ if (connectCursor != null && connected.replay_complete_marker !== true && reconnectingIncompleteReplay) {
1456
+ // The connection that established this replay boundary died before its
1457
+ // marker. Restart legacy semantics from the last confirmed cursor: do
1458
+ // not retain buffered/seen rows that would discard the replayed match.
1459
+ pendingReplayMatches.length = 0;
1460
+ pendingReplaySupersession = null;
1461
+ seen.clear();
1462
+ cursor = replayFrontier;
1463
+ }
1464
+ replayPending = connectCursor != null && connected.replay_complete_marker === true;
1465
+ continue;
1466
+ }
1467
+
1378
1468
  // BOT-1067: a degraded marker frame (e.g. the relay fell back to bounded
1379
1469
  // re-query polling during a Realtime outage) is folded into the receipt's
1380
1470
  // degraded array. The wait CONTINUES — delivery is just late and flagged —
@@ -1400,13 +1490,60 @@ export async function runWaitLoop({
1400
1490
  return finalize("error", { exitCode: EXIT.INTERNAL, error: parsed.error || "stream_error" });
1401
1491
  }
1402
1492
 
1493
+ if (frame.event === "replay_complete") {
1494
+ replayPending = false;
1495
+ replayFrontier = cursor;
1496
+ const pendingMatch = pendingReplayMatches
1497
+ .sort((a, b) => BigInt(a.seqKey) < BigInt(b.seqKey) ? -1 : BigInt(a.seqKey) > BigInt(b.seqKey) ? 1 : 0)[0];
1498
+ const replayCandidate = pendingMatch &&
1499
+ (!pendingReplaySupersession || BigInt(pendingMatch.seqKey) < BigInt(pendingReplaySupersession.seqKey))
1500
+ ? { kind: "match", ...pendingMatch }
1501
+ : pendingReplaySupersession ? { kind: "superseded", ...pendingReplaySupersession } : null;
1502
+ if (replayCandidate) {
1503
+ const candidateAtMs = Date.parse(replayCandidate.signal.created_at);
1504
+ if (restoredAlarm != null && Number.isFinite(candidateAtMs) && restoredAlarm.atMs < candidateAtMs) {
1505
+ return fireAlarm(restoredAlarm);
1506
+ }
1507
+ if (replayCandidate.kind === "superseded") return finalizeSuperseded(replayCandidate.signal);
1508
+ const terminal = await finalize("matched", {
1509
+ exitCode: EXIT.MATCHED,
1510
+ matched: [replayCandidate.entry],
1511
+ });
1512
+ if (onFrame) {
1513
+ await onFrame(replayCandidate.signal, {
1514
+ cursor: replayCandidate.seqKey,
1515
+ terminalReceipt: terminal.receipt,
1516
+ exitCode: terminal.exitCode,
1517
+ });
1518
+ }
1519
+ return terminal;
1520
+ }
1521
+ if (replayOnlyThroughMs != null) return fireAlarm();
1522
+ if (onFrame) await onFrame(null, { cursor, replayComplete: true });
1523
+ continue;
1524
+ }
1403
1525
  const signal = decodeSignal(frame);
1404
1526
  if (!signal || signal.seq == null) continue;
1405
1527
  const seqKey = String(signal.seq);
1528
+ // Replayed rows can arrive after a higher live row. Keep the highest
1529
+ // observed sequence so replay_complete can safely promote the whole
1530
+ // confirmed range, even when that live row is de-duplicated below.
1531
+ if (cursor == null || BigInt(seqKey) > BigInt(cursor)) cursor = seqKey;
1532
+ if (!replayPending) replayFrontier = cursor;
1406
1533
  if (seen.has(seqKey)) continue; // exactly-once across reconnects
1407
1534
  seen.add(seqKey);
1408
- cursor = seqKey;
1409
- if (onFrame) onFrame(signal);
1535
+ const checkpointNonterminalFrame = async () => {
1536
+ if (!replayPending && onFrame) await onFrame(signal, { cursor: seqKey });
1537
+ };
1538
+ const beforeReplayDeadline = replayOnlyThroughMs == null || (
1539
+ typeof signal.created_at === "string" &&
1540
+ Number.isFinite(Date.parse(signal.created_at)) &&
1541
+ Date.parse(signal.created_at) <= replayOnlyThroughMs
1542
+ );
1543
+ // Do not checkpoint the replay cursor yet. A crash after a pre-match
1544
+ // checkpoint would skip this very frame on recovery. Non-matches become
1545
+ // durable only after matching is complete; a match carries its terminal
1546
+ // receipt in the same checkpoint callback below (BOT-1656).
1410
1547
 
1411
1548
  // BOT-1249: a `wait_superseded` spine signal targeting THIS registration is
1412
1549
  // terminal (exit 8). reconcile_waits_to_canonical() emits it — in seq order,
@@ -1430,43 +1567,11 @@ export async function runWaitLoop({
1430
1567
  waitSessionId != null &&
1431
1568
  signal.payload && signal.payload.wait_session_id === waitSessionId
1432
1569
  ) {
1433
- // Re-arm the re-registration from the wait's ORIGINAL arm cursor (cursor_start),
1434
- // NOT this supersession's seq. `seq` is allocated at INSERT, not COMMIT, and a
1435
- // live frame can overtake the replay backlog — so a supersession can be delivered
1436
- // to us AHEAD of a match that was actually eligible (a chat/pr/ci signal for a
1437
- // mixed `any` wait, or a canonical signal that arrived during the alias window).
1438
- // If we re-armed from the supersession's (higher) seq we'd skip that match on
1439
- // re-registration. cursor_start is the seq this wait armed from, so re-arming here
1440
- // gives the re-registered wait EXACTLY the coverage the original registration had
1441
- // — strictly better than re-arming from the last-seen (higher) cursor, which is
1442
- // all a normal same-wait reconnect gets. (The one residual gap — a match whose row
1443
- // was allocated a seq BELOW cursor_start but committed after registration read
1444
- // max(seq) — is the pre-existing BOT-989 register-after-emit race that any
1445
- // reconnect shares; it is out of scope here and tracked separately. We do NOT
1446
- // re-arm from 0, which would spuriously re-match stale pre-arm signals.) Falls
1447
- // back to the current cursor if the emitter didn't stamp cursor_start (defensive —
1448
- // reconcile always does).
1449
- const cs = signal.payload.cursor_start;
1450
- if (cs != null && /^\d+$/.test(String(cs))) cursor = String(cs);
1451
- // A superseded CLAIM wait lost its grantable queue entry: reconcile_waits_to_
1452
- // canonical() dequeues the queued resource_locks row before abandoning the
1453
- // session. A plain re-run re-registers the wait_session but does NOT recreate
1454
- // that queue row, so grant_free_lock would have nothing to promote and the
1455
- // re-armed claim would hang to --timeout. Surface the lock(s) to re-acquire so
1456
- // the caller re-queues (acquire_lock) BEFORE re-parking (BOT-1228 Codex round-3
1457
- // P1). Broadcast waits need no queue entry, so this is empty for them.
1458
- const reacquire = conditions
1459
- .filter((c) => c.type === "lock" && c.params.claim)
1460
- .map((c) => ({ subtype: c.params.subtype, host: c.params.host, slot: c.params.slot }));
1461
- return finalize("superseded", {
1462
- exitCode: EXIT.SUPERSEDED,
1463
- state: {
1464
- superseded: true,
1465
- status: typeof signal.payload.status === "string" ? signal.payload.status : null,
1466
- },
1467
- // Top-level (survives receipt truncation) — see buildReceipt.
1468
- reacquireLock: reacquire,
1469
- });
1570
+ if (replayPending) {
1571
+ if (beforeReplayDeadline) pendingReplaySupersession ??= { signal, seqKey };
1572
+ continue;
1573
+ }
1574
+ return finalizeSuperseded(signal);
1470
1575
  }
1471
1576
 
1472
1577
  // BOT-1148: a beacon-driven capacity staleness transition is handled before the
@@ -1477,7 +1582,10 @@ export async function runWaitLoop({
1477
1582
  if (disposition === "exit") {
1478
1583
  return finalize("error", { exitCode: EXIT.BACKEND, error: "capacity_source_stale" });
1479
1584
  }
1480
- if (disposition === "continue") continue;
1585
+ if (disposition === "continue") {
1586
+ await checkpointNonterminalFrame();
1587
+ continue;
1588
+ }
1481
1589
  // "fallthrough": let matchFrame see the frame — a recovery may grant via
1482
1590
  // free_slots, and a raw `event` condition may match a stale frame (the
1483
1591
  // `capacity` matcher itself never grants on source_stale).
@@ -1491,11 +1599,12 @@ export async function runWaitLoop({
1491
1599
  if (debug) {
1492
1600
  debug(`bb-wait: dropped ${signal.signal_type} signal (seq ${signal.seq}) — tenant ${signal.tenant_id} ≠ session tenant ${sessionTenant ?? "(unresolved)"} [BOT-1259 fail-closed]`);
1493
1601
  }
1602
+ await checkpointNonterminalFrame();
1494
1603
  continue;
1495
1604
  }
1496
1605
 
1497
1606
  const matched = matchFrame(frame, conditions, waitSessionId, sessionTenant);
1498
- if (matched) {
1607
+ if (matched && beforeReplayDeadline) {
1499
1608
  const entry = {
1500
1609
  condition_id: matched.id,
1501
1610
  signal_type: signal.signal_type,
@@ -1507,11 +1616,26 @@ export async function runWaitLoop({
1507
1616
  // BOT-1577: a ranked capacity grant records this wait's position/tier/score.
1508
1617
  const grant = capacityGrantForReceipt(signal, waitSessionId);
1509
1618
  if (grant) entry.grant = grant;
1510
- return finalize("matched", {
1619
+ if (replayPending) {
1620
+ pendingReplayMatches.push({ entry, seqKey, signal });
1621
+ continue;
1622
+ }
1623
+ const terminal = await finalize("matched", {
1511
1624
  exitCode: EXIT.MATCHED,
1512
1625
  matched: [entry],
1513
1626
  });
1627
+ if (onFrame) {
1628
+ await onFrame(signal, {
1629
+ cursor: seqKey,
1630
+ terminalReceipt: terminal.receipt,
1631
+ exitCode: terminal.exitCode,
1632
+ });
1633
+ }
1634
+ return terminal;
1514
1635
  }
1636
+ // The frame was completely evaluated and cannot be an eligible terminal
1637
+ // result. It is now safe to advance the persisted replay floor.
1638
+ await checkpointNonterminalFrame();
1515
1639
  }
1516
1640
 
1517
1641
  // Stream ended without a match: reconnect from the cursor.
@@ -1521,6 +1645,6 @@ export async function runWaitLoop({
1521
1645
  }
1522
1646
  const waitMs = backoff(reconnects);
1523
1647
  const raced = await Promise.race([deadlinePromise, sleepFn(waitMs).then(() => ({ __retry: true }))]);
1524
- if (raced && raced.__deadline) return fireAlarm();
1648
+ if (raced && raced.__deadline) return fireReplayGraceExpired();
1525
1649
  }
1526
1650
  }