@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/wait-core.mjs +62 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbuddy/cli",
3
- "version": "1.44.0",
3
+ "version": "1.44.1",
4
4
  "description": "BotBuddy — Swarm coordination CLI for multi-agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
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 = pendingMatch &&
1704
- (!pendingReplaySupersession || BigInt(pendingMatch.seqKey) < BigInt(pendingReplaySupersession.seqKey))
1705
- ? { kind: "match", ...pendingMatch }
1706
- : pendingReplaySupersession ? { kind: "superseded", ...pendingReplaySupersession } : null;
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: a beacon-driven capacity staleness transition is handled before the
1780
- // normal match — a stale signal is never itself a grant, and a fast exit / marker
1781
- // fold happens here (host-pinned) rather than through matchFrame.
1782
- if (signal.signal_type === "container_capacity" && signal.payload && "source_stale" in signal.payload) {
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" });