@botbuddy/cli 1.33.1 → 1.33.2
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/stack.mjs +59 -5
package/package.json
CHANGED
package/src/stack.mjs
CHANGED
|
@@ -805,7 +805,13 @@ export async function waitForLease(leaseId, isDone, isFailed, { timeoutSec, auth
|
|
|
805
805
|
|
|
806
806
|
const isActive = (s) => s === "active";
|
|
807
807
|
const nonQueued = (s) => s != null && s !== "queued";
|
|
808
|
-
|
|
808
|
+
// BOT-1822 (Codex round 3 P2): `reap_failed` is a distinct terminal wake
|
|
809
|
+
// (accept_stack_lease_receipt's fenced-reap-failure branch) alongside the real
|
|
810
|
+
// DB states "reaping"/"reaped" — a wait parked on isReaped as its failure
|
|
811
|
+
// predicate must end on it too, or a `reap_failed` wake that arrives while
|
|
812
|
+
// still waiting for activation is silently dropped and the wait parks until
|
|
813
|
+
// its full timeout instead of surfacing the failure immediately.
|
|
814
|
+
const isReaped = (s) => s === "reaping" || s === "reaped" || s === "reap_failed";
|
|
809
815
|
|
|
810
816
|
/** Read-only pressure gate used before local managed-stack provisioning. */
|
|
811
817
|
export async function runLocalExecPreflight(opts, runCommand = runDockerCommand) {
|
|
@@ -1833,8 +1839,38 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
1833
1839
|
return cleanupResult = { ok: false, error: release?.error || release?.data?.code || "release failed" };
|
|
1834
1840
|
}
|
|
1835
1841
|
if (release.data.state === "reaped") return cleanupResult = { ok: true, state: "reaped" };
|
|
1836
|
-
|
|
1842
|
+
// BOT-1822 (Codex P2 round 2, extended round 3): `reap_failed` is synthetic
|
|
1843
|
+
// (never a real DB state), so a Helper's failed receipt landing in the split
|
|
1844
|
+
// second between this release RPC returning and the wait below registering
|
|
1845
|
+
// cannot be replayed from the spine's initial snapshot the way every other
|
|
1846
|
+
// signal type can — the wake is already behind the cursor, and the initial
|
|
1847
|
+
// snapshot only surfaces the durable `reaping` state, which neither
|
|
1848
|
+
// predicate accepts. Recover the durable failure reason directly:
|
|
1849
|
+
// `stack_leases.error` (which `get_stack_lease` already returns) holds the
|
|
1850
|
+
// most recent failure message regardless of which live signal was or
|
|
1851
|
+
// wasn't seen.
|
|
1852
|
+
const durableReapFailure = (read) => (read?.ok && read.data?.success && read.data.state === "reaping" && read.data.error)
|
|
1853
|
+
? { ok: false, error: `the lease is still reaping and may still hold containers — ${read.data.error}` }
|
|
1854
|
+
: null;
|
|
1855
|
+
// BOT-1822 (Codex round 3 P2): check BEFORE opening the wait, not only after
|
|
1856
|
+
// it times out — a failure already persisted must be reported at once
|
|
1857
|
+
// instead of burning the entire --reap-timeout waiting for a wake that will
|
|
1858
|
+
// never arrive because it predates the wait's registration.
|
|
1859
|
+
const preCheck = durableReapFailure(await api.get(leaseId));
|
|
1860
|
+
if (preCheck) return cleanupResult = preCheck;
|
|
1861
|
+
// BOT-1822 (Codex P2): a signed reap that FAILS on a provisioned (fenced) lease
|
|
1862
|
+
// emits `state: "reap_failed"` — a distinct terminal outcome, never a real DB
|
|
1863
|
+
// state — instead of leaving this wait to park until --reap-timeout on a reap
|
|
1864
|
+
// that already failed. Report that specific outcome instead of the generic
|
|
1865
|
+
// "was not proven" timeout message.
|
|
1866
|
+
const reaped = await wait(leaseId, (state) => state === "reaped", (state) => state === "reap_failed", { timeoutSec: opts.reapTimeout, auth: rpc.auth });
|
|
1837
1867
|
if (reaped?.woke || reaped?.state === "reaped") return cleanupResult = { ok: true, state: "reaped" };
|
|
1868
|
+
if (reaped?.failed) return cleanupResult = { ok: false, error: "the signed reap failed and the lease may still hold containers (fenced) — see stack hygiene" };
|
|
1869
|
+
// Belt-and-braces: the same race can still slip between the pre-check above
|
|
1870
|
+
// and the wait registering (or the wait's own live-signal miss), so recheck
|
|
1871
|
+
// once more before falling back to a generic timeout/error message.
|
|
1872
|
+
const postCheck = durableReapFailure(await api.get(leaseId));
|
|
1873
|
+
if (postCheck) return cleanupResult = postCheck;
|
|
1838
1874
|
return cleanupResult = { ok: false, error: reaped?.timeout ? `signed reap did not arrive within ${opts.reapTimeout}s` : (reaped?.error || "signed reap was not proven") };
|
|
1839
1875
|
})();
|
|
1840
1876
|
return cleanupPromise;
|
|
@@ -1944,11 +1980,29 @@ export async function runStackLifecycle(opts, childArgv, adapters = {}) {
|
|
|
1944
1980
|
}
|
|
1945
1981
|
if (state !== "active") {
|
|
1946
1982
|
activeWaitAbort = new AbortController();
|
|
1947
|
-
|
|
1983
|
+
// BOT-1822 (Codex P2 round 2): a provision failure now emits a live
|
|
1984
|
+
// `reaping` signal (accept_stack_lease_receipt's provision-failed branch),
|
|
1985
|
+
// so this wait must treat it as terminal too — not just `reaped` — or the
|
|
1986
|
+
// live-wake fix is defeated: the run would still burn the FULL
|
|
1987
|
+
// provisionTimeout before the belt-and-braces recheck below ever notices
|
|
1988
|
+
// the lease is no longer provisioning. isReaped (== "reaping" || "reaped")
|
|
1989
|
+
// already covers both; cmdUp's equivalent waits use it for this reason.
|
|
1990
|
+
const active = await wait(leaseId, (s) => s === "active", isReaped, { timeoutSec: provisionTimeout, auth: rpc.auth, signal: activeWaitAbort.signal });
|
|
1948
1991
|
activeWaitAbort = null;
|
|
1949
1992
|
if (active?.interrupted || receivedSignal) { const cleanup = await requestCleanup(); return { exitCode: SIGNAL_EXIT[receivedSignal], outcome: "interrupted", leaseId, cleanup }; }
|
|
1950
|
-
if (active?.timeout) {
|
|
1951
|
-
|
|
1993
|
+
if (active?.timeout) {
|
|
1994
|
+
// BOT-1822 belt-and-braces: the zero-poll wait can still miss a live
|
|
1995
|
+
// `stack_lease` signal (an older server before this fix, a relay hiccup,
|
|
1996
|
+
// BOT-1816's class of dropped wakes) even though the lease genuinely went
|
|
1997
|
+
// active. One direct read before declaring a timeout tells that apart from
|
|
1998
|
+
// a lease that truly never provisioned, instead of reaping a healthy stack.
|
|
1999
|
+
const recheck = await api.get(leaseId);
|
|
2000
|
+
if (!(recheck?.ok && recheck.data?.success && recheck.data.state === "active")) {
|
|
2001
|
+
const cleanup = await requestCleanup();
|
|
2002
|
+
return { exitCode: EXIT.TIMEOUT, outcome: "timeout", leaseId, error: `physical provision did not reach active in ${provisionTimeout}s`, cleanup };
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
if (active?.failed || active?.error || active?.auth) { const cleanup = await requestCleanup(); return { exitCode: active?.auth ? EXIT.AUTH : EXIT.LEASE_FAILED, outcome: "error", leaseId, error: active?.error || `lease ${active?.state ?? "failed"} before reaching active`, cleanup }; }
|
|
1952
2006
|
}
|
|
1953
2007
|
const current = await api.get(leaseId);
|
|
1954
2008
|
if (!current?.ok || !current.data?.success || current.data.state !== "active" || !current.data.connection || typeof current.data.connection !== "object") {
|