@botbuddy/cli 1.44.0 → 1.44.1
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/package.json +1 -1
- package/src/wait-core.mjs +62 -8
package/package.json
CHANGED
package/src/wait-core.mjs
CHANGED
|
@@ -1276,6 +1276,10 @@ export async function runWaitLoop({
|
|
|
1276
1276
|
// This prevents a terminal receipt from either skipping lower replay matches or
|
|
1277
1277
|
// replaying its own match on a chained wait.
|
|
1278
1278
|
const pendingReplayMatches = [];
|
|
1279
|
+
// Capacity frames also carry state: a lower-sequence fresh beacon can clear a
|
|
1280
|
+
// later stale confirmation. Defer their state transitions until replay_complete
|
|
1281
|
+
// so they are folded in spine sequence order, just like terminal matches.
|
|
1282
|
+
const pendingReplayBeaconFrames = [];
|
|
1279
1283
|
let pendingReplaySupersession = null;
|
|
1280
1284
|
const degraded = [];
|
|
1281
1285
|
let reconnects = 0;
|
|
@@ -1662,6 +1666,7 @@ export async function runWaitLoop({
|
|
|
1662
1666
|
// marker. Restart legacy semantics from the last confirmed cursor: do
|
|
1663
1667
|
// not retain buffered/seen rows that would discard the replayed match.
|
|
1664
1668
|
pendingReplayMatches.length = 0;
|
|
1669
|
+
pendingReplayBeaconFrames.length = 0;
|
|
1665
1670
|
pendingReplaySupersession = null;
|
|
1666
1671
|
seen.clear();
|
|
1667
1672
|
cursor = replayFrontier;
|
|
@@ -1698,18 +1703,60 @@ export async function runWaitLoop({
|
|
|
1698
1703
|
if (frame.event === "replay_complete") {
|
|
1699
1704
|
replayPending = false;
|
|
1700
1705
|
replayFrontier = cursor;
|
|
1706
|
+
let pendingBeaconExit = null;
|
|
1707
|
+
// A replay terminal is a point-in-time receipt. Beacon state after an
|
|
1708
|
+
// earlier match/supersession cannot affect that receipt.
|
|
1709
|
+
let terminalSeq = [
|
|
1710
|
+
...pendingReplayMatches.map(({ seqKey }) => seqKey),
|
|
1711
|
+
pendingReplaySupersession?.seqKey,
|
|
1712
|
+
].filter(Boolean).sort((a, b) => BigInt(a) < BigInt(b) ? -1 : BigInt(a) > BigInt(b) ? 1 : 0)[0] ?? null;
|
|
1713
|
+
for (const pendingBeaconFrame of pendingReplayBeaconFrames
|
|
1714
|
+
.sort((a, b) => BigInt(a.seqKey) < BigInt(b.seqKey) ? -1 : BigInt(a.seqKey) > BigInt(b.seqKey) ? 1 : 0)) {
|
|
1715
|
+
if (terminalSeq != null && BigInt(pendingBeaconFrame.seqKey) >= BigInt(terminalSeq)) break;
|
|
1716
|
+
// Recovery replays may contain rows created after the original deadline.
|
|
1717
|
+
// They are only transport catch-up, never evidence for the terminal
|
|
1718
|
+
// receipt, so they must not alter stale confirmation state either.
|
|
1719
|
+
if (!pendingBeaconFrame.beforeReplayDeadline) continue;
|
|
1720
|
+
const disposition = applyBeaconStale(pendingBeaconFrame.signal);
|
|
1721
|
+
if (disposition === "exit") {
|
|
1722
|
+
pendingBeaconExit = pendingBeaconFrame;
|
|
1723
|
+
terminalSeq = pendingBeaconFrame.seqKey;
|
|
1724
|
+
break;
|
|
1725
|
+
}
|
|
1726
|
+
if (disposition !== "fallthrough" || !pendingBeaconFrame.beforeReplayDeadline) continue;
|
|
1727
|
+
if (signalTenantBlocked(pendingBeaconFrame.signal, sessionTenant)) continue;
|
|
1728
|
+
const matched = matchFrame(pendingBeaconFrame.frame, conditions, waitSessionId, sessionTenant);
|
|
1729
|
+
if (!matched) continue;
|
|
1730
|
+
const entry = {
|
|
1731
|
+
condition_id: matched.id,
|
|
1732
|
+
signal_type: pendingBeaconFrame.signal.signal_type,
|
|
1733
|
+
seq: pendingBeaconFrame.signal.seq,
|
|
1734
|
+
subject_key: pendingBeaconFrame.signal.subject_key,
|
|
1735
|
+
payload: pendingBeaconFrame.signal.payload ?? null,
|
|
1736
|
+
provenance: "spine",
|
|
1737
|
+
};
|
|
1738
|
+
const grant = capacityGrantForReceipt(pendingBeaconFrame.signal, waitSessionId);
|
|
1739
|
+
if (grant) entry.grant = grant;
|
|
1740
|
+
pendingReplayMatches.push({ ...pendingBeaconFrame, entry });
|
|
1741
|
+
terminalSeq = pendingBeaconFrame.seqKey;
|
|
1742
|
+
}
|
|
1743
|
+
pendingReplayBeaconFrames.length = 0;
|
|
1701
1744
|
const pendingMatch = pendingReplayMatches
|
|
1702
1745
|
.sort((a, b) => BigInt(a.seqKey) < BigInt(b.seqKey) ? -1 : BigInt(a.seqKey) > BigInt(b.seqKey) ? 1 : 0)[0];
|
|
1703
|
-
const replayCandidate =
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1746
|
+
const replayCandidate = [
|
|
1747
|
+
pendingMatch && { kind: "match", ...pendingMatch },
|
|
1748
|
+
pendingReplaySupersession && { kind: "superseded", ...pendingReplaySupersession },
|
|
1749
|
+
pendingBeaconExit && { kind: "beacon_stale", ...pendingBeaconExit },
|
|
1750
|
+
].filter(Boolean).sort((a, b) => BigInt(a.seqKey) < BigInt(b.seqKey) ? -1 : BigInt(a.seqKey) > BigInt(b.seqKey) ? 1 : 0)[0] ?? null;
|
|
1707
1751
|
if (replayCandidate) {
|
|
1708
1752
|
const candidateAtMs = Date.parse(replayCandidate.signal.created_at);
|
|
1709
1753
|
if (restoredAlarm != null && Number.isFinite(candidateAtMs) && restoredAlarm.atMs < candidateAtMs) {
|
|
1710
1754
|
return fireAlarm(restoredAlarm);
|
|
1711
1755
|
}
|
|
1712
1756
|
if (replayCandidate.kind === "superseded") return finalizeSuperseded(replayCandidate.signal);
|
|
1757
|
+
if (replayCandidate.kind === "beacon_stale") {
|
|
1758
|
+
return finalize("error", { exitCode: EXIT.BACKEND, error: "capacity_source_stale" });
|
|
1759
|
+
}
|
|
1713
1760
|
const terminal = await finalizeMatch(replayCandidate.entry, replayCandidate.signal);
|
|
1714
1761
|
if (onFrame) {
|
|
1715
1762
|
await onFrame(replayCandidate.signal, {
|
|
@@ -1776,10 +1823,17 @@ export async function runWaitLoop({
|
|
|
1776
1823
|
return finalizeSuperseded(signal);
|
|
1777
1824
|
}
|
|
1778
1825
|
|
|
1779
|
-
// BOT-1148:
|
|
1780
|
-
//
|
|
1781
|
-
//
|
|
1782
|
-
|
|
1826
|
+
// BOT-1148/BOT-2100: every capacity frame for a pinned host participates in
|
|
1827
|
+
// beacon-staleness handling. `source_stale:true` arms the fast-fail, while a
|
|
1828
|
+
// later normal capacity frame proves the host is fresh again even when the
|
|
1829
|
+
// server omits the redundant `source_stale:false` field. Without that latter
|
|
1830
|
+
// path, an old stale transition could win its confirm timer despite fresh
|
|
1831
|
+
// beacons continuing to publish capacity frames.
|
|
1832
|
+
if (signal.signal_type === "container_capacity" && signal.payload) {
|
|
1833
|
+
if (replayPending) {
|
|
1834
|
+
pendingReplayBeaconFrames.push({ frame, signal, seqKey, beforeReplayDeadline });
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1783
1837
|
const disposition = applyBeaconStale(signal);
|
|
1784
1838
|
if (disposition === "exit") {
|
|
1785
1839
|
return finalize("error", { exitCode: EXIT.BACKEND, error: "capacity_source_stale" });
|