@botbuddy/cli 1.19.0 → 1.19.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/stack.mjs +60 -28
package/package.json
CHANGED
package/src/stack.mjs
CHANGED
|
@@ -665,6 +665,7 @@ export async function cmdUp(opts, {
|
|
|
665
665
|
callTool = callToolJson,
|
|
666
666
|
authProvider = stackAuthHeader,
|
|
667
667
|
localProvisionFn = localProvision,
|
|
668
|
+
waitFn = waitForLease,
|
|
668
669
|
emitResult = emit,
|
|
669
670
|
} = {}) {
|
|
670
671
|
let slot;
|
|
@@ -720,7 +721,7 @@ export async function cmdUp(opts, {
|
|
|
720
721
|
return emitResult(buildReceipt({ command: "up", outcome: "queued", lease_id: leaseId, state, host_key: d.host_key, slot, queued: true, queue_position: d.queue_position, holders: d.holders }), opts, EXIT.QUEUED);
|
|
721
722
|
}
|
|
722
723
|
// Park zero-poll until the lease leaves the queue (granted in BOT-1187 order).
|
|
723
|
-
const parked = await
|
|
724
|
+
const parked = await waitFn(leaseId, nonQueued, () => false, { timeoutSec: opts.timeout, auth });
|
|
724
725
|
if (parked.timeout) return emitResult(buildReceipt({ command: "up", outcome: "timeout", lease_id: leaseId, state: "queued", slot, error: `parked ${opts.timeout}s without capacity` }), opts, EXIT.TIMEOUT);
|
|
725
726
|
if (parked.auth) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: "unauthorized on wait stream" }), opts, EXIT.AUTH);
|
|
726
727
|
if (parked.error) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: parked.error }), opts, EXIT.BACKEND);
|
|
@@ -762,36 +763,67 @@ export async function cmdUp(opts, {
|
|
|
762
763
|
connection: connectionWithDockerTarget({}, localDockerTarget),
|
|
763
764
|
});
|
|
764
765
|
if (!reserved.ok || !reserved.data?.success) {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
766
|
+
// A Helper can win the reservation race by taking the queued provision
|
|
767
|
+
// job between `request_stack_lease` and `reserve_stack_lease`. That is NOT
|
|
768
|
+
// a failure — nothing local has started, and the Helper owns (or is about
|
|
769
|
+
// to activate) a live stack for this same lease. Cancelling here would
|
|
770
|
+
// abandon that stack until idle reaping. Two shapes signal the takeover,
|
|
771
|
+
// both from `reserve_stack_lease` which checks lease state before job
|
|
772
|
+
// status (BOT-1587):
|
|
773
|
+
// • PROVISION_JOB_HELPER_CLAIMED — Helper claimed the job, lease still
|
|
774
|
+
// `provisioning`: wait for it to reach active.
|
|
775
|
+
// • LEASE_NOT_PROVISIONING with state `active` — the Helper both claimed
|
|
776
|
+
// AND activated before reserve acquired its locks: already live, so
|
|
777
|
+
// skip the wait (the edge-triggered stream would park until timeout on
|
|
778
|
+
// an already-active lease) and go straight to the read.
|
|
779
|
+
// Either way, follow the Helper to the shared authoritative final read
|
|
780
|
+
// below; only a genuine reserve failure is terminal.
|
|
781
|
+
const helperClaimed = reserved.data?.code === "PROVISION_JOB_HELPER_CLAIMED";
|
|
782
|
+
const helperAlreadyActive = reserved.data?.code === "LEASE_NOT_PROVISIONING" && reserved.data?.state === "active";
|
|
783
|
+
if (helperClaimed || helperAlreadyActive) {
|
|
784
|
+
if (!helperAlreadyActive) {
|
|
785
|
+
const active = await waitFn(leaseId, isActive, isReaped, { timeoutSec: opts.timeout, auth });
|
|
786
|
+
if (active.timeout) return emitResult(buildReceipt({ command: "up", outcome: "timeout", lease_id: leaseId, state, slot, error: `a Helper claimed the provision job but it did not reach active in ${opts.timeout}s` }), opts, EXIT.TIMEOUT);
|
|
787
|
+
if (active.auth) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: "unauthorized on wait stream" }), opts, EXIT.AUTH);
|
|
788
|
+
if (active.failed) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, state: active.state, error: "lease was reaped before it became active" }), opts, EXIT.LEASE_FAILED);
|
|
789
|
+
if (active.error) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: active.error }), opts, EXIT.BACKEND);
|
|
790
|
+
}
|
|
791
|
+
// Fall through to the authoritative `get_stack_lease` read below — do
|
|
792
|
+
// NOT run the local provisioner; the Helper owns this stack.
|
|
793
|
+
} else {
|
|
794
|
+
const cancelled = await callTool("cancel_unclaimed_stack_lease", { lease_id: leaseId });
|
|
795
|
+
const leaseCancellation = cancelled.ok && cancelled.data?.success
|
|
796
|
+
? { success: true, state: cancelled.data.state, provision_job_cancelled: cancelled.data.provision_job_cancelled === true }
|
|
797
|
+
: { success: false, error: cancelled.error || cancelled.data?.code || "atomic cancellation failed" };
|
|
798
|
+
return emitResult(buildReceipt({
|
|
799
|
+
command: "up", outcome: "error", lease_id: leaseId, state, slot,
|
|
800
|
+
error: reserved.data?.code || reserved.error || "could not reserve the provision job for local execution",
|
|
801
|
+
lease_cancellation: leaseCancellation,
|
|
802
|
+
}), opts, EXIT.LEASE_FAILED);
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
let conn;
|
|
806
|
+
try {
|
|
807
|
+
conn = connectionWithDockerTarget(localProvisionFn(opts, localDockerTarget), localDockerTarget);
|
|
808
|
+
} catch (e) {
|
|
809
|
+
// Once the local provisioner has run, `supabase start` may have created
|
|
810
|
+
// (or fully started) containers even on a nonzero exit or a status-parse
|
|
811
|
+
// failure. Cancelling here would reap the lease and free the slot while
|
|
812
|
+
// that stack is still up — the exact orphaned/contended-slot hazard this
|
|
813
|
+
// change closes. Leave the lease FENCED (provisioning, reserved to us) so
|
|
814
|
+
// `botbuddy stack done` / the reaper tears the stack down first (BOT-1421
|
|
815
|
+
// review); do NOT cancel.
|
|
816
|
+
return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: e.message }), opts, EXIT.LEASE_FAILED);
|
|
817
|
+
}
|
|
818
|
+
const act = await callTool("activate_stack_lease", { lease_id: leaseId, connection: conn });
|
|
819
|
+
if (!act.ok || !act.data?.success) {
|
|
820
|
+
return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: act.error || act.data?.code || "activate failed" }), opts, EXIT.BACKEND);
|
|
821
|
+
}
|
|
791
822
|
}
|
|
792
823
|
} else {
|
|
793
|
-
const active = await
|
|
824
|
+
const active = await waitFn(leaseId, isActive, isReaped, { timeoutSec: opts.timeout, auth });
|
|
794
825
|
if (active.timeout) return emitResult(buildReceipt({ command: "up", outcome: "timeout", lease_id: leaseId, state, slot, error: `provisioning did not reach active in ${opts.timeout}s (Helper may be down — retry with --local-exec)` }), opts, EXIT.TIMEOUT);
|
|
826
|
+
if (active.auth) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: "unauthorized on wait stream" }), opts, EXIT.AUTH);
|
|
795
827
|
if (active.failed) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, state: active.state, error: "lease was reaped before it became active" }), opts, EXIT.LEASE_FAILED);
|
|
796
828
|
if (active.error) return emitResult(buildReceipt({ command: "up", outcome: "error", lease_id: leaseId, error: active.error }), opts, EXIT.BACKEND);
|
|
797
829
|
}
|